You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by ni...@apache.org on 2018/07/22 03:01:51 UTC
[incubator-servicecomb-saga] 01/02: SCB-692 Restore the old sage
files
This is an automated email from the ASF dual-hosted git repository.
ningjiang pushed a commit to branch SCB-692
in repository https://gitbox.apache.org/repos/asf/incubator-servicecomb-saga.git
commit 2a19cf5a43dba3630bf666094baa489612384522
Author: Willem Jiang <ji...@huawei.com>
AuthorDate: Fri Jul 20 18:52:12 2018 +0800
SCB-692 Restore the old sage files
---
docs/api/api.md | 168 +++++
docs/old_saga.md | 53 ++
integration-tests/coverage-aggregate/pom.xml | 20 +
pom.xml | 41 ++
saga-core/pom.xml | 103 +++
.../servicecomb/saga/core/BackwardRecovery.java | 49 ++
.../apache/servicecomb/saga/core/Compensation.java | 33 +
.../saga/core/CompensationTaskConsumer.java | 69 ++
.../servicecomb/saga/core/CompositeSagaLog.java | 40 ++
.../saga/core/CompositeSagaResponse.java | 48 ++
.../apache/servicecomb/saga/core/Descriptive.java | 25 +
.../apache/servicecomb/saga/core/EventContext.java | 28 +
.../servicecomb/saga/core/EventEnvelope.java | 47 ++
.../apache/servicecomb/saga/core/EventStore.java | 25 +
.../servicecomb/saga/core/FailedSagaResponse.java | 50 ++
.../org/apache/servicecomb/saga/core/Fallback.java | 25 +
.../servicecomb/saga/core/FallbackPolicy.java | 62 ++
.../servicecomb/saga/core/ForwardRecovery.java | 53 ++
.../servicecomb/saga/core/GraphBasedSaga.java | 132 ++++
.../apache/servicecomb/saga/core/IdGenerator.java | 25 +
.../saga/core/LoggingRecoveryPolicy.java | 46 ++
.../servicecomb/saga/core/LongIdGenerator.java | 30 +
.../servicecomb/saga/core/NoOpSagaRequest.java | 107 ++++
.../apache/servicecomb/saga/core/Operation.java | 33 +
.../servicecomb/saga/core/PersistentLog.java | 22 +
.../servicecomb/saga/core/PersistentStore.java | 26 +
.../servicecomb/saga/core/RecoveryPolicy.java | 26 +
.../servicecomb/saga/core/RequestProcessTask.java | 67 ++
.../servicecomb/saga/core/RestOperation.java | 59 ++
.../org/apache/servicecomb/saga/core/Saga.java | 27 +
.../apache/servicecomb/saga/core/SagaContext.java | 36 ++
.../servicecomb/saga/core/SagaContextImpl.java | 133 ++++
.../servicecomb/saga/core/SagaDefinition.java | 25 +
.../apache/servicecomb/saga/core/SagaEndTask.java | 48 ++
.../servicecomb/saga/core/SagaEndedEvent.java | 38 ++
.../apache/servicecomb/saga/core/SagaEvent.java | 39 ++
.../servicecomb/saga/core/SagaException.java | 29 +
.../org/apache/servicecomb/saga/core/SagaLog.java | 22 +
.../apache/servicecomb/saga/core/SagaRequest.java | 48 ++
.../servicecomb/saga/core/SagaRequestImpl.java | 153 +++++
.../apache/servicecomb/saga/core/SagaResponse.java | 61 ++
.../saga/core/SagaStartFailedException.java | 25 +
.../servicecomb/saga/core/SagaStartTask.java | 56 ++
.../servicecomb/saga/core/SagaStartedEvent.java | 46 ++
.../apache/servicecomb/saga/core/SagaState.java | 27 +
.../org/apache/servicecomb/saga/core/SagaTask.java | 31 +
.../servicecomb/saga/core/SagaTaskFactory.java | 98 +++
.../saga/core/SuccessfulSagaResponse.java | 43 ++
.../apache/servicecomb/saga/core/TaskConsumer.java | 29 +
.../apache/servicecomb/saga/core/TaskRunner.java | 72 +++
.../apache/servicecomb/saga/core/ToJsonFormat.java | 26 +
.../apache/servicecomb/saga/core/Transaction.java | 27 +
.../saga/core/TransactionAbortedEvent.java | 57 ++
.../saga/core/TransactionCompensatedEvent.java | 56 ++
.../saga/core/TransactionEndedEvent.java | 56 ++
.../saga/core/TransactionFailedException.java | 29 +
.../saga/core/TransactionStartedEvent.java | 45 ++
.../saga/core/TransactionTaskConsumer.java | 123 ++++
.../apache/servicecomb/saga/core/Transport.java | 22 +
.../saga/core/TransportFailedException.java | 28 +
.../saga/core/actors/ActorBasedSaga.java | 65 ++
.../saga/core/actors/ActorBasedSagaFactory.java | 83 +++
.../saga/core/actors/CompletionCallbackActor.java | 69 ++
.../saga/core/actors/EventContextImpl.java | 60 ++
.../servicecomb/saga/core/actors/RequestActor.java | 206 ++++++
.../saga/core/actors/RequestActorBuilder.java | 108 ++++
.../saga/core/actors/RequestActorContext.java | 82 +++
.../saga/core/actors/messages/AbortMessage.java | 37 ++
.../core/actors/messages/AbortRecoveryMessage.java | 33 +
.../core/actors/messages/CompensateMessage.java | 32 +
.../messages/CompensationRecoveryMessage.java | 22 +
.../saga/core/actors/messages/FailMessage.java | 33 +
.../saga/core/actors/messages/Message.java | 21 +
.../saga/core/actors/messages/TransactMessage.java | 39 ++
.../messages/TransactionRecoveryMessage.java | 32 +
.../core/application/SagaExecutionComponent.java | 87 +++
.../saga/core/application/SagaFactory.java | 30 +
.../application/interpreter/FromJsonFormat.java | 24 +
.../interpreter/RestRequestChecker.java | 55 ++
.../saga/core/dag/ByLevelTraveller.java | 83 +++
.../saga/core/dag/FromLeafTraversalDirection.java | 38 ++
.../saga/core/dag/FromRootTraversalDirection.java | 38 ++
.../saga/core/dag/GraphBasedSagaFactory.java | 80 +++
.../servicecomb/saga/core/dag/GraphBuilder.java | 109 ++++
.../saga/core/dag/GraphCycleDetector.java | 25 +
.../saga/core/dag/GraphCycleDetectorImpl.java | 70 +++
.../org/apache/servicecomb/saga/core/dag/Node.java | 86 +++
.../core/dag/SingleLeafDirectedAcyclicGraph.java | 38 ++
.../servicecomb/saga/core/dag/Traveller.java | 29 +
.../saga/core/dag/TraversalDirection.java | 29 +
.../infrastructure/ContextAwareEventStore.java | 56 ++
.../saga/infrastructure/EmbeddedEventStore.java | 58 ++
.../servicecomb/saga/transports/RestTransport.java | 27 +
.../saga/transports/TransportFactory.java | 23 +
.../saga/core/BackwardRecoveryTest.java | 57 ++
.../servicecomb/saga/core/CompensationImpl.java | 27 +
.../saga/core/CompositeSagaLogTest.java | 49 ++
.../saga/core/CompositeSagaResponseTest.java | 73 +++
.../apache/servicecomb/saga/core/DummyEvent.java | 31 +
.../servicecomb/saga/core/FallbackPolicyTest.java | 91 +++
.../servicecomb/saga/core/ForwardRecoveryTest.java | 59 ++
.../servicecomb/saga/core/LongIdGeneratorTest.java | 36 ++
.../servicecomb/saga/core/RestOperationTest.java | 89 +++
.../servicecomb/saga/core/RetrySagaLogTest.java | 80 +++
.../servicecomb/saga/core/SagaEndTaskTest.java | 47 ++
.../servicecomb/saga/core/SagaEventMatcher.java | 68 ++
.../saga/core/SagaExecutionComponentTestBase.java | 250 ++++++++
.../servicecomb/saga/core/SagaIntegrationTest.java | 687 ++++++++++++++++++++
.../servicecomb/saga/core/SagaStartTaskTest.java | 64 ++
.../servicecomb/saga/core/TransactionImpl.java | 27 +
.../ActorBasedSagaExecutionComponentTest.java | 31 +
.../core/actors/ActorBasedSagaIntegrationTest.java | 691 +++++++++++++++++++++
.../core/actors/CompletionCallbackActorTest.java | 138 ++++
.../saga/core/actors/EventContextImplTest.java | 93 +++
.../saga/core/actors/RequestActorBuilderTest.java | 173 ++++++
.../saga/core/actors/RequestActorTest.java | 399 ++++++++++++
.../dag/DirectedAcyclicGraphTraversalTest.java | 102 +++
.../dag/GraphBasedSagaExecutionComponentTest.java | 33 +
.../saga/core/dag/GraphBuilderTest.java | 148 +++++
.../saga/core/dag/GraphCycleDetectorTest.java | 73 +++
.../apache/servicecomb/saga/core/dag/NodeTest.java | 80 +++
.../infrastructure/ContextAwareEventStoreTest.java | 43 ++
saga-core/src/test/resources/application.conf | 21 +
saga-core/src/test/resources/log4j2.xml | 30 +
saga-demo/conditional-transaction-demo/README.md | 174 ++++++
.../conditional-transaction-demo-tests/pom.xml | 301 +++++++++
.../saga/demo/tests/ConditionalTransactionIT.java | 155 +++++
.../docker-compose.yaml | 75 +++
.../conditional-transaction-demo/inventory/pom.xml | 74 +++
.../inventory/InventoryApplication.java | 29 +
.../transaction/inventory/InventoryController.java | 89 +++
.../inventory/InventoryControllerTest.java | 79 +++
.../membership/pom.xml | 74 +++
.../membership/MembershipApplication.java | 29 +
.../membership/MembershipController.java | 62 ++
.../conditional-transaction-demo/payment/pom.xml | 75 +++
.../transaction/payment/PaymentApplication.java | 29 +
.../transaction/payment/PaymentController.java | 84 +++
.../transaction/payment/PaymentControllerTest.java | 61 ++
saga-demo/conditional-transaction-demo/pom.xml | 40 ++
.../conditional-transaction-demo/supplier/pom.xml | 74 +++
.../transaction/supplier/SupplierApplication.java | 29 +
.../transaction/supplier/SupplierController.java | 52 ++
.../src/main/resources/META-INF/LICENSE.txt | 202 ++++++
.../src/main/resources/META-INF/NOTICE.txt | 11 +
.../dependency-free-transaction-demo/README.md | 168 +++++
.../car-rental-service/pom.xml | 85 +++
.../saga/demo/car/rental/CarRentalApplication.java | 32 +
.../saga/demo/car/rental/CarRentalController.java | 100 +++
.../src/main/resources/microservice.yaml | 30 +
.../demo-tests/pom.xml | 333 ++++++++++
.../apache/servicecomb/saga/demo/tests/DemoIT.java | 150 +++++
.../docker-compose.yaml | 101 +++
.../flight-booking-service/pom.xml | 85 +++
.../flight/booking/FlightBookingApplication.java | 32 +
.../flight/booking/FlightBookingController.java | 77 +++
.../src/main/resources/microservice.yaml | 30 +
.../hotel-reservation-service/pom.xml | 85 +++
.../reservation/HotelReservationApplication.java | 32 +
.../reservation/HotelReservationController.java | 78 +++
.../src/main/resources/microservice.yaml | 30 +
.../payment-service/pom.xml | 85 +++
.../saga/demo/payment/PaymentApplication.java | 32 +
.../saga/demo/payment/PaymentController.java | 81 +++
.../src/main/resources/microservice.yaml | 32 +
saga-demo/dependency-free-transaction-demo/pom.xml | 67 ++
saga-demo/pom.xml | 2 +
.../saga/demo/pack/booking/BookingController.java | 1 +
saga-discovery/pom.xml | 37 ++
.../saga-discovery-servicecenter/pom.xml | 124 ++++
.../center/ServiceCenterDiscoveryConfig.java | 43 ++
.../src/main/resources/META-INF/spring.factories | 18 +
.../src/main/resources/microservice.yaml | 30 +
.../discovery/service/center/DummyController.java | 56 ++
.../center/ServiceCenterDiscoveryApplication.java | 32 +
.../ServiceCenterDiscoveryRestTransportTest.java | 132 ++++
.../src/test/resources/log4j2-test.xml | 30 +
.../src/test/resources/registry.yaml | 23 +
saga-format/pom.xml | 74 +++
.../saga/core/FailedSagaRequestContext.java | 40 ++
.../servicecomb/saga/core/JacksonToJsonFormat.java | 65 ++
.../servicecomb/saga/core/SagaRequestContext.java | 37 ++
.../saga/core/SuccessfulSagaRequestContext.java | 41 ++
.../servicecomb/saga/format/ChildrenExtractor.java | 55 ++
.../servicecomb/saga/format/JacksonFallback.java | 64 ++
.../saga/format/JacksonFromJsonFormat.java | 58 ++
.../saga/format/JacksonRestCompensation.java | 52 ++
.../saga/format/JacksonRestFallback.java | 43 ++
.../saga/format/JacksonRestOperation.java | 62 ++
.../saga/format/JacksonRestTransaction.java | 36 ++
.../saga/format/JacksonSagaEventFormat.java | 114 ++++
.../saga/format/JsonFailedSagaResponse.java | 29 +
.../saga/format/JsonRestSagaRequest.java | 72 +++
.../saga/format/JsonSagaDefinition.java | 60 ++
.../servicecomb/saga/format/JsonSagaRequest.java | 39 ++
.../saga/format/JsonSuccessfulSagaResponse.java | 30 +
.../servicecomb/saga/format/SagaEventFormat.java | 25 +
.../servicecomb/saga/format/TransportAware.java | 26 +
.../saga/format/ChildrenExtractorTest.java | 75 +++
.../saga/format/JacksonFromJsonFormatTest.java | 261 ++++++++
.../saga/format/JacksonRestOperationTest.java | 69 ++
.../saga/format/JsonRestSagaRequestTest.java | 95 +++
.../saga/format/JsonSagaDefinitionTest.java | 34 +
.../saga/format/SagaEventFormatTest.java | 180 ++++++
saga-performance/README.md | 47 ++
saga-performance/images/grafana.png | Bin 0 -> 15508 bytes
saga-performance/scripts/saga.jmx | 289 +++++++++
saga-spring/pom.xml | 233 +++++++
.../saga/spring/JpaPersistentStore.java | 69 ++
.../servicecomb/saga/spring/SagaController.java | 222 +++++++
.../servicecomb/saga/spring/SagaEventEntity.java | 85 +++
.../servicecomb/saga/spring/SagaEventRepo.java | 44 ++
.../saga/spring/SagaExecutionQueryService.java | 160 +++++
.../saga/spring/SagaRecoveryListener.java | 37 ++
.../saga/spring/SagaShutdownListener.java | 42 ++
.../saga/spring/SagaSpringApplication.java | 39 ++
.../servicecomb/saga/spring/SagaSpringConfig.java | 136 ++++
saga-spring/src/main/resources/META-INF/aop.xml | 25 +
.../src/main/resources/META-INF/spring.factories | 20 +
saga-spring/src/main/resources/application.conf | 88 +++
saga-spring/src/main/resources/application.yaml | 30 +
saga-spring/src/main/resources/log4j2.xml | 30 +
.../src/main/resources/schema-postgresql.sql | 26 +
.../ActorBasedSagaSpringApplicationTest.java | 25 +
.../GraphBasedSagaSpringApplicationTest.java | 25 +
.../saga/spring/GreetingController.java | 42 ++
.../servicecomb/saga/spring/SagaRecoveryTest.java | 181 ++++++
.../saga/spring/SagaServiceDiscoveryTest.java | 113 ++++
.../saga/spring/SagaSpringApplicationTestBase.java | 390 ++++++++++++
saga-spring/src/test/resources/data.sql | 29 +
saga-spring/src/test/resources/log4j2-test.xml | 30 +
saga-spring/src/test/resources/registry.yaml | 23 +
saga-web/pom.xml | 130 ++++
.../servicecomb/saga/web/SagaWebApplication.java | 34 +
saga-web/src/main/resources/application.yaml | 29 +
saga-web/src/main/resources/microservice.yaml | 31 +
saga-web/src/main/resources/static/css/request.css | 66 ++
saga-web/src/main/resources/static/css/style.css | 41 ++
saga-web/src/main/resources/static/detail.html | 174 ++++++
saga-web/src/main/resources/static/index.html | 31 +
saga-web/src/main/resources/static/js/date.js | 660 ++++++++++++++++++++
saga-web/src/main/resources/static/js/request.js | 294 +++++++++
saga-web/src/main/resources/static/js/table.js | 128 ++++
saga-web/src/main/resources/static/request.html | 232 +++++++
saga-web/src/main/resources/static/result.html | 67 ++
transports/pom.xml | 39 ++
transports/transport-httpclient-spring/pom.xml | 80 +++
.../saga/transports/HttpClientTransportConfig.java | 40 ++
.../src/main/resources/META-INF/spring.factories | 19 +
.../saga/transports/RestTransportTest.java | 123 ++++
.../src/test/resources/log4j2.xml | 30 +
transports/transport-httpclient/pom.xml | 63 ++
.../transports/httpclient/HttpClientTransport.java | 121 ++++
.../httpclient/HttpClientTransportTest.java | 192 ++++++
.../src/test/resources/log4j2.xml | 30 +
transports/transport-resttemplate/pom.xml | 71 +++
.../resttemplate/RestTemplateTransport.java | 121 ++++
257 files changed, 19897 insertions(+)
diff --git a/docs/api/api.md b/docs/api/api.md
new file mode 100755
index 0000000..e0a9e8f
--- /dev/null
+++ b/docs/api/api.md
@@ -0,0 +1,168 @@
+# Saga API
+### Post transaction and compensation requests to Saga
+```
+POST /requests
+```
+
+#### Description
+
+1. Define requests in order and recovery policy by JSON format as below,put them to body.
+```
+{
+ "policy": "",
+ "requests": [
+ {
+ "id": "",
+ "type": "",
+ "serviceName": "",
+ "parents": [
+
+ ],
+ "transaction": {
+ "method": "",
+ "path": "",
+ "params": {
+
+ }
+ },
+ "compensation": {
+ "method": "",
+ "path": "",
+ "params": {
+
+ }
+ }
+ }
+ ]
+}
+```
+JSON parameters:
+- policy - support `BackwardRecovery` or `ForwardRecovery`.
+- requests - transactions array.
+ - id - request id. It should be unique among this collection of requests.
+ - type - support `rest` for now.
+ - serviceName - user-defined service name.
+ - parents - request ids. It means this request is only executed after all requests in the parents field are completed.
+ - transaction - user-defined transaction that executed by the Saga.
+ - method - user-defined, HTTP method.
+ - path - user-defined, HTTP path.
+ - params - support `form`,`json`,`body`,`query`.
+ - compensation - user-defined compensation that executed by the Saga.
+ - method - user-defined, HTTP method.
+ - path - user-defined, HTTP path.
+ - params - support `form`,`json`,`body`,`query`.
+
+2. Set content type to `text/plain`.
+
+3. Send them to Saga service.
+
+#### Example request
+```
+curl -XPOST -H "Content-Type: text/plain" -d @./request.json http://<docker.host.ip:saga.port>/requests
+```
+
+#### Example response
+```
+success
+```
+
+#### Status codes
+- **200** – no error
+- **400** – bad parameter
+- **500** – server error
+
+
+### Get all the Saga events
+```
+GET /events
+```
+
+#### Description
+Get all the Saga events.
+
+#### Example request
+```
+curl -XGET http://<docker.host.ip:saga.port>/events
+```
+
+#### Example response
+```
+{
+ "88658e73-eff5-4d31-887e-019201d6b560": [
+ {
+ "id": 1,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:40Z",
+ "type": "SagaStartedEvent",
+ "contentJson": "{\"policy\": \"BackwardRecovery\", \"requests\": [{\"id\": \"request-car\", \"type\": \"rest\", \"serviceName\": \"car-rental-service\", \"transaction\": {\"path\": \"/rentals\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/rentals\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}}, {\"id\": \"request-hotel\", \"type\": \"rest\", \"serviceName\": \"hotel-reservation-service [...]
+ },
+ {
+ "id": 2,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:40Z",
+ "type": "TransactionStartedEvent",
+ "contentJson": "{\"id\": \"request-flight\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"flight-booking-service\", \"transaction\": {\"path\": \"/bookings\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/bookings\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}"
+ },
+ {
+ "id": 3,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:40Z",
+ "type": "TransactionStartedEvent",
+ "contentJson": "{\"id\": \"request-car\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"car-rental-service\", \"transaction\": {\"path\": \"/rentals\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/rentals\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}"
+ },
+ {
+ "id": 4,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:40Z",
+ "type": "TransactionStartedEvent",
+ "contentJson": "{\"id\": \"request-hotel\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"hotel-reservation-service\", \"transaction\": {\"path\": \"/reservations\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/reservations\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}"
+ },
+ {
+ "id": 5,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:40Z",
+ "type": "TransactionEndedEvent",
+ "contentJson": "{\"request\": {\"id\": \"request-flight\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"flight-booking-service\", \"transaction\": {\"path\": \"/bookings\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/bookings\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}, \"response\": {\"body\": \"{\\n \\\"statusCode\\\" [...]
+ },
+ {
+ "id": 6,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:40Z",
+ "type": "TransactionEndedEvent",
+ "contentJson": "{\"request\": {\"id\": \"request-hotel\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"hotel-reservation-service\", \"transaction\": {\"path\": \"/reservations\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/reservations\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}, \"response\": {\"body\": \"{\\n \\\"stat [...]
+ },
+ {
+ "id": 7,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:41Z",
+ "type": "TransactionEndedEvent",
+ "contentJson": "{\"request\": {\"id\": \"request-car\", \"type\": \"rest\", \"parents\": [], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"car-rental-service\", \"transaction\": {\"path\": \"/rentals\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/rentals\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}, \"response\": {\"body\": \"{\\n \\\"statusCode\\\": 200,\\n [...]
+ },
+ {
+ "id": 8,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:41Z",
+ "type": "TransactionStartedEvent",
+ "contentJson": "{\"id\": \"request-payment\", \"type\": \"rest\", \"parents\": [\"request-car\", \"request-flight\", \"request-hotel\"], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"payment-service\", \"transaction\": {\"path\": \"/payments\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/payments\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}"
+ },
+ {
+ "id": 9,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:41Z",
+ "type": "TransactionEndedEvent",
+ "contentJson": "{\"request\": {\"id\": \"request-payment\", \"type\": \"rest\", \"parents\": [\"request-car\", \"request-flight\", \"request-hotel\"], \"fallback\": {\"type\": \"NOP\"}, \"serviceName\": \"payment-service\", \"transaction\": {\"path\": \"/payments\", \"method\": \"post\", \"params\": {\"form\": {\"customerId\": \"mike\"}}}, \"compensation\": {\"path\": \"/payments\", \"method\": \"put\", \"params\": {\"form\": {\"customerId\": \"mike\"}}, \"retries\": 3}}, \"r [...]
+ },
+ {
+ "id": 10,
+ "sagaId": "88658e73-eff5-4d31-887e-019201d6b560",
+ "creationTime": "2017-09-15T01:15:41Z",
+ "type": "SagaEndedEvent",
+ "contentJson": "{}"
+ }
+ ]
+}
+```
+
+#### Status codes
+- **200** – no error
+
diff --git a/docs/old_saga.md b/docs/old_saga.md
new file mode 100755
index 0000000..2b79d22
--- /dev/null
+++ b/docs/old_saga.md
@@ -0,0 +1,53 @@
+# Previous Saga's Documentation
+## Major Architecture of Saga
+* saga-core(transaction and compensation handling logic)
+* saga-format(data serialization and deserialization)
+* saga-transports(communication protocol implementation such as rest or rpc in the future)
+* saga-discovery(service discovery)
+* saga-spring(restful service framework)
+
+
+
+## Prerequisites
+You will need:
+1. [Oracle JDK 1.8+][jdk]
+2. [Maven 3.x][maven]
+3. [Docker][docker]
+4. [PostgreSQL][postgres]
+5. [Service Center(optional)][service_center]
+6. [Docker compose(optional)][docker_compose]
+7. [Docker machine(optional)][docker_machine]
+
+[jdk]: http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
+[maven]: https://maven.apache.org/install.html
+[docker]: https://www.docker.com/get-docker
+[postgres]: https://www.postgresql.org/download/
+[service_center]: https://github.com/apache/incubator-servicecomb-service-center
+[docker_compose]: https://docs.docker.com/compose/install/
+[docker_machine]: https://docs.docker.com/machine/install-machine/
+
+## Building
+Download the source code.
+```
+git clone https://github.com/apache/incubator-servicecomb-saga.git
+```
+
+Enter the Saga root directory,build Saga project by maven command and generate a docker image named saga-spring in local.
+```
+mvn package -DskipTests -Pdocker
+```
+
+## Run Services
+A `docker-compose.yaml` file is provided to start Saga services and its dependencies(Service center and Mysql) as docker containers.
+User also can configure specified Service center or Mysql in `docker-compose.yaml`.
+
+Enter the Saga root directory, run all service images using command,
+```
+docker-compose up
+```
+
+## Reference API
+See [Saga API](docs/api/api.md) for details.
+
+## Example
+See [Saga demo](https://github.com/apache/incubator-servicecomb-saga/tree/master/saga-demo) for details.
diff --git a/integration-tests/coverage-aggregate/pom.xml b/integration-tests/coverage-aggregate/pom.xml
index ccd5b9a..ef4df2e 100644
--- a/integration-tests/coverage-aggregate/pom.xml
+++ b/integration-tests/coverage-aggregate/pom.xml
@@ -31,6 +31,26 @@
<dependencies>
<dependency>
<groupId>org.apache.servicecomb.saga</groupId>
+ <artifactId>saga-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga</groupId>
+ <artifactId>saga-format</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga</groupId>
+ <artifactId>saga-spring</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga.transports</groupId>
+ <artifactId>transport-httpclient</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga.discovery</groupId>
+ <artifactId>saga-discovery-servicecenter</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga</groupId>
<artifactId>omega-context</artifactId>
</dependency>
<dependency>
diff --git a/pom.xml b/pom.xml
index 8f91a95..99a0fe2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -31,7 +31,13 @@
<version>0.3.0-SNAPSHOT</version>
<modules>
+ <module>saga-core</module>
+ <module>transports</module>
+ <module>saga-spring</module>
<module>docker-build-config</module>
+ <module>saga-format</module>
+ <module>saga-discovery</module>
+ <module>saga-web</module>
<module>omega</module>
<module>alpha</module>
<module>pack-contracts</module>
@@ -124,6 +130,41 @@
<dependencies>
<dependency>
<groupId>org.apache.servicecomb.saga</groupId>
+ <artifactId>saga-core</artifactId>
+ <version>0.3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga</groupId>
+ <artifactId>saga-format</artifactId>
+ <version>0.3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga</groupId>
+ <artifactId>saga-spring</artifactId>
+ <version>0.3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga.transports</groupId>
+ <artifactId>transport-httpclient</artifactId>
+ <version>0.3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga.transports</groupId>
+ <artifactId>transport-httpclient-spring</artifactId>
+ <version>0.3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga.transports</groupId>
+ <artifactId>transport-resttemplate</artifactId>
+ <version>0.3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga.discovery</groupId>
+ <artifactId>saga-discovery-servicecenter</artifactId>
+ <version>0.3.0-SNAPSHOT</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.servicecomb.saga</groupId>
<artifactId>omega-context</artifactId>
<version>0.3.0-SNAPSHOT</version>
</dependency>
diff --git a/saga-core/pom.xml b/saga-core/pom.xml
new file mode 100644
index 0000000..1cc1c61
--- /dev/null
+++ b/saga-core/pom.xml
@@ -0,0 +1,103 @@
+<?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>saga</artifactId>
+ <groupId>org.apache.servicecomb.saga</groupId>
+ <version>0.3.0-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>saga-core</artifactId>
+ <name>Saga::Core</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>com.lmax</groupId>
+ <artifactId>disruptor</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-slf4j-impl</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe.akka</groupId>
+ <artifactId>akka-actor_2.12</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.typesafe.akka</groupId>
+ <artifactId>akka-slf4j_2.12</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.kamon</groupId>
+ <artifactId>kamon-core_2.12</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>io.kamon</groupId>
+ <artifactId>kamon-annotation_2.12</artifactId>
+ </dependency>
+
+ <!-- test dependencies -->
+ <dependency>
+ <groupId>com.typesafe.akka</groupId>
+ <artifactId>akka-testkit_2.12</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.scalatest</groupId>
+ <artifactId>scalatest_2.12</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-lang3</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.awaitility</groupId>
+ <artifactId>awaitility</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.github.seanyinx</groupId>
+ <artifactId>unit-scaffolding</artifactId>
+ </dependency>
+ </dependencies>
+
+</project>
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/BackwardRecovery.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/BackwardRecovery.java
new file mode 100644
index 0000000..e46de2f
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/BackwardRecovery.java
@@ -0,0 +1,49 @@
+/*
+ * 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.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+@EnableKamon
+public class BackwardRecovery implements RecoveryPolicy {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ @Segment(name = "backwardPolicy", category = "application", library = "kamon")
+ @Override
+ public SagaResponse apply(SagaTask task, SagaRequest request, SagaResponse parentResponse) {
+ try {
+ return request.transaction().send(request.serviceName(), parentResponse);
+ } catch (Exception e) {
+ log.error("Applying {} policy due to failure in transaction {} of service {}",
+ description(),
+ request.transaction(),
+ request.serviceName(),
+ e
+ );
+
+ task.abort(request, e);
+ throw e;
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Compensation.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Compensation.java
new file mode 100644
index 0000000..f642011
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Compensation.java
@@ -0,0 +1,33 @@
+/*
+ * 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.saga.core;
+
+public interface Compensation extends Operation {
+
+ Compensation SAGA_START_COMPENSATION = new Compensation() {
+ };
+
+ Compensation SAGA_END_COMPENSATION = new Compensation() {
+ };
+
+ int DEFAULT_RETRIES = 3;
+
+ default int retries() {
+ return DEFAULT_RETRIES;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompensationTaskConsumer.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompensationTaskConsumer.java
new file mode 100644
index 0000000..736c34d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompensationTaskConsumer.java
@@ -0,0 +1,69 @@
+/*
+ * 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.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.apache.servicecomb.saga.core.dag.Node;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+class CompensationTaskConsumer implements TaskConsumer {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ private final Map<String, SagaTask> tasks;
+ private final SagaContext sagaContext;
+
+ CompensationTaskConsumer(Map<String, SagaTask> tasks, SagaContext sagaContext) {
+ this.tasks = tasks;
+ this.sagaContext = sagaContext;
+ }
+
+ @Override
+ public void consume(Collection<Node<SagaRequest>> nodes) {
+ for (Node<SagaRequest> node : nodes) {
+ SagaRequest request = node.value();
+
+ if (sagaContext.isTransactionCompleted(request)) {
+ log.info("Starting request {} id={}", request.serviceName(), request.id());
+ tasks.get(request.task()).compensate(request);
+ log.info("Completed request {} id={}", request.serviceName(), request.id());
+ }
+ }
+ }
+
+ @Override
+ public boolean replay(Collection<Node<SagaRequest>> nodes) {
+
+ for (Iterator<Node<SagaRequest>> iterator = nodes.iterator(); iterator.hasNext(); ) {
+ SagaRequest request = iterator.next().value();
+ if (sagaContext.isCompensationCompleted(request)) {
+ log.info("Skipped completed compensation id={} operation={} while replay", request.id(), request.transaction());
+ iterator.remove();
+ } else if (!sagaContext.isTransactionCompleted(request)) {
+ // this transaction never started
+ log.info("Skipped pending transaction id={} operation={} while replay", request.id(), request.transaction());
+ iterator.remove();
+ }
+ }
+ return !nodes.isEmpty();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaLog.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaLog.java
new file mode 100644
index 0000000..017129a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaLog.java
@@ -0,0 +1,40 @@
+/*
+ * 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.saga.core;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+class CompositeSagaLog implements SagaLog {
+
+ private final SagaLog embedded;
+ private final PersistentLog persistent;
+
+ CompositeSagaLog(SagaLog embedded, PersistentLog persistent) {
+ this.embedded = embedded;
+ this.persistent = persistent;
+ }
+
+ @Segment(name = "compositeSagaLog", category = "application", library = "kamon")
+ @Override
+ public void offer(SagaEvent sagaEvent) {
+ persistent.offer(sagaEvent);
+ embedded.offer(sagaEvent);
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaResponse.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaResponse.java
new file mode 100644
index 0000000..02ab48c
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/CompositeSagaResponse.java
@@ -0,0 +1,48 @@
+/*
+ * 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.saga.core;
+
+import java.util.Collection;
+import java.util.Optional;
+
+public class CompositeSagaResponse implements SagaResponse {
+ private final Collection<SagaResponse> responses;
+
+ public CompositeSagaResponse(Collection<SagaResponse> responses) {
+ this.responses = responses;
+ }
+
+ @Override
+ public boolean succeeded() {
+ return responses.stream().allMatch(SagaResponse::succeeded);
+ }
+
+ @Override
+ public String body() {
+ Optional<String> reduce = responses.stream()
+ .map(SagaResponse::body)
+ .reduce((a, b) -> a + ", " + b)
+ .map(combined -> "[" + combined + "]");
+
+ return reduce.orElse("{}");
+ }
+
+ public Collection<SagaResponse> responses() {
+ return responses;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Descriptive.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Descriptive.java
new file mode 100644
index 0000000..bd367af
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Descriptive.java
@@ -0,0 +1,25 @@
+/*
+ * 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.saga.core;
+
+interface Descriptive {
+
+ default String description() {
+ return getClass().getSimpleName();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventContext.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventContext.java
new file mode 100644
index 0000000..79b0c3e
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventContext.java
@@ -0,0 +1,28 @@
+/*
+ * 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.saga.core;
+
+public interface EventContext {
+ void beginTransaction(SagaRequest request);
+
+ void endTransaction(SagaRequest request, SagaResponse response);
+
+ void abortTransaction(SagaRequest request, SagaResponse response);
+
+ void compensateTransaction(SagaRequest request, SagaResponse response);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventEnvelope.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventEnvelope.java
new file mode 100644
index 0000000..9283b33
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventEnvelope.java
@@ -0,0 +1,47 @@
+/*
+ * 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.saga.core;
+
+public class EventEnvelope {
+
+ public final long id;
+ public final long timestamp;
+ public final SagaEvent event;
+
+ public EventEnvelope(long id, SagaEvent event) {
+ this.id = id;
+ this.event = event;
+ this.timestamp = System.currentTimeMillis();
+ }
+
+ // TODO: 8/21/2017 this class seems to be useless to saga
+ public EventEnvelope(long id, long timestamp, SagaEvent event) {
+ this.id = id;
+ this.timestamp = timestamp;
+ this.event = event;
+ }
+
+ @Override
+ public String toString() {
+ return "EventEnvelope{" +
+ "id=" + id +
+ ", timestamp=" + timestamp +
+ ", event={" + event +
+ "}}";
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventStore.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventStore.java
new file mode 100644
index 0000000..7b7e6e2
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/EventStore.java
@@ -0,0 +1,25 @@
+/*
+ * 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.saga.core;
+
+public interface EventStore extends SagaLog, Iterable<SagaEvent> {
+
+ void populate(Iterable<EventEnvelope> events);
+
+ long size();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/FailedSagaResponse.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/FailedSagaResponse.java
new file mode 100644
index 0000000..15d1701
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/FailedSagaResponse.java
@@ -0,0 +1,50 @@
+/*
+ * 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.saga.core;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public class FailedSagaResponse implements SagaResponse {
+
+ private final String body;
+
+ public FailedSagaResponse(String body) {
+ this.body = body;
+ }
+
+ public FailedSagaResponse(Throwable e) {
+ this.body = stackTrace(e);
+ }
+
+ @Override
+ public boolean succeeded() {
+ return false;
+ }
+
+ @Override
+ public String body() {
+ return body;
+ }
+
+ private String stackTrace(Throwable e) {
+ StringWriter writer = new StringWriter();
+ e.printStackTrace(new PrintWriter(writer));
+ return writer.toString();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Fallback.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Fallback.java
new file mode 100644
index 0000000..fca68a0
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Fallback.java
@@ -0,0 +1,25 @@
+/*
+ * 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.saga.core;
+
+public interface Fallback extends Operation {
+
+ Fallback NOP_FALLBACK = () -> TYPE_NOP;
+
+ String type();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/FallbackPolicy.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/FallbackPolicy.java
new file mode 100644
index 0000000..48ad67d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/FallbackPolicy.java
@@ -0,0 +1,62 @@
+/*
+ * 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.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class FallbackPolicy {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final int retryDelay;
+
+ public FallbackPolicy(int retryDelay) {
+ this.retryDelay = retryDelay;
+ }
+
+ public SagaResponse apply(String address, Compensation compensation, Fallback fallback) {
+ for (int i = 0; isRetryable(i, compensation) && !isInterrupted(); i++) {
+ try {
+ return compensation.send(address);
+ } catch (Exception e) {
+ log.error("Failed to send compensation to {}", address, e);
+ sleep(retryDelay);
+ }
+ }
+
+ log.warn("Falling back after {} failures sending compensation to {}", compensation.retries(), address);
+ return fallback.send(address);
+ }
+
+ private boolean isRetryable(int i, Compensation compensation) {
+ return compensation.retries() < 0 || i <= compensation.retries();
+ }
+
+ private boolean isInterrupted() {
+ return Thread.currentThread().isInterrupted();
+ }
+
+ private void sleep(int delay) {
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException ignored) {
+ Thread.currentThread().interrupt();
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/ForwardRecovery.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/ForwardRecovery.java
new file mode 100644
index 0000000..c783990
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/ForwardRecovery.java
@@ -0,0 +1,53 @@
+/*
+ * 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.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ForwardRecovery implements RecoveryPolicy {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ @Override
+ public SagaResponse apply(SagaTask task, SagaRequest request, SagaResponse parentResponse) {
+ try {
+ do {
+ try {
+ return request.transaction().send(request.serviceName(), parentResponse);
+ } catch (Exception e) {
+ log.error("Applying {} policy due to failure in transaction {} of service {}",
+ description(),
+ request.transaction(),
+ request.serviceName(),
+ e
+ );
+ Thread.sleep(request.failRetryDelayMilliseconds());
+ }
+ } while (true);
+ } catch (InterruptedException ignored) {
+ log.warn("Applying {} interrupted in transaction {} of service {}",
+ description(),
+ request.transaction(),
+ request.serviceName(),
+ ignored);
+ throw new TransactionFailedException(ignored);
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/GraphBasedSaga.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/GraphBasedSaga.java
new file mode 100644
index 0000000..f4544e3
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/GraphBasedSaga.java
@@ -0,0 +1,132 @@
+/*
+ * 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.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.Executors;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import org.apache.servicecomb.saga.core.dag.ByLevelTraveller;
+import org.apache.servicecomb.saga.core.dag.FromLeafTraversalDirection;
+import org.apache.servicecomb.saga.core.dag.FromRootTraversalDirection;
+import org.apache.servicecomb.saga.core.dag.SingleLeafDirectedAcyclicGraph;
+import org.apache.servicecomb.saga.core.dag.TraversalDirection;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class GraphBasedSaga implements Saga {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final EventStore eventStore;
+ private final Map<String, SagaTask> tasks;
+
+ private final TaskRunner transactionTaskRunner;
+ private final TaskRunner compensationTaskRunner;
+ private final SagaContext sagaContext;
+ private volatile SagaState currentTaskRunner;
+
+ GraphBasedSaga(EventStore eventStore,
+ Map<String, SagaTask> tasks,
+ SagaContext sagaContext,
+ SingleLeafDirectedAcyclicGraph<SagaRequest> sagaTaskGraph) {
+
+ this(eventStore, Executors.newFixedThreadPool(5), tasks, sagaContext, sagaTaskGraph);
+ }
+
+ public GraphBasedSaga(EventStore eventStore,
+ Executor executor,
+ Map<String, SagaTask> tasks,
+ SagaContext sagaContext,
+ SingleLeafDirectedAcyclicGraph<SagaRequest> sagaTaskGraph) {
+
+ this.eventStore = eventStore;
+ this.tasks = tasks;
+
+ this.transactionTaskRunner = new TaskRunner(
+ traveller(sagaTaskGraph, new FromRootTraversalDirection<>()),
+ new TransactionTaskConsumer(
+ tasks,
+ sagaContext,
+ new ExecutorCompletionService<>(executor)));
+
+ this.sagaContext = sagaContext;
+ this.compensationTaskRunner = new TaskRunner(
+ traveller(sagaTaskGraph, new FromLeafTraversalDirection<>()),
+ new CompensationTaskConsumer(tasks, sagaContext));
+
+ currentTaskRunner = transactionTaskRunner;
+ }
+
+ @Override
+ @Segment(name = "runSaga", category = "application", library = "kamon")
+ public SagaResponse run() {
+ SagaResponse response = SagaResponse.EMPTY_RESPONSE;
+ log.info("Starting Saga");
+ do {
+ try {
+ currentTaskRunner.run();
+ } catch (TransactionFailedException e) {
+ response = new FailedSagaResponse(e);
+ log.error("Failed to run operation", e);
+ currentTaskRunner = compensationTaskRunner;
+
+ sagaContext.handleHangingTransactions(request -> {
+ tasks.get(request.task()).commit(request, sagaContext.responseOf(request.parents()));
+ tasks.get(request.task()).compensate(request);
+ });
+ }
+ } while (currentTaskRunner.hasNext());
+ log.info("Completed Saga");
+ return response;
+ }
+
+ @Override
+ public void play() {
+ log.info("Start playing events");
+ gatherEvents(eventStore);
+
+ transactionTaskRunner.replay();
+
+ if (sagaContext.isCompensationStarted()) {
+ currentTaskRunner = compensationTaskRunner;
+ compensationTaskRunner.replay();
+ }
+
+ log.info("Completed playing events");
+ }
+
+ private void gatherEvents(Iterable<SagaEvent> events) {
+ for (SagaEvent event : events) {
+ event.gatherTo(sagaContext);
+ }
+ }
+
+ private ByLevelTraveller<SagaRequest> traveller(
+ SingleLeafDirectedAcyclicGraph<SagaRequest> sagaTaskGraph,
+ TraversalDirection<SagaRequest> traversalDirection) {
+
+ return new ByLevelTraveller<>(sagaTaskGraph, traversalDirection);
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/IdGenerator.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/IdGenerator.java
new file mode 100644
index 0000000..2add260
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/IdGenerator.java
@@ -0,0 +1,25 @@
+/*
+ * 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.saga.core;
+
+import java.io.Serializable;
+
+public interface IdGenerator<T extends Serializable> {
+
+ T nextId();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/LoggingRecoveryPolicy.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/LoggingRecoveryPolicy.java
new file mode 100644
index 0000000..9173eca
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/LoggingRecoveryPolicy.java
@@ -0,0 +1,46 @@
+/*
+ * 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.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+
+@EnableKamon
+public class LoggingRecoveryPolicy implements RecoveryPolicy {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+
+ private final RecoveryPolicy recoveryPolicy;
+
+ public LoggingRecoveryPolicy(RecoveryPolicy recoveryPolicy) {
+ this.recoveryPolicy = recoveryPolicy;
+ }
+
+ @Segment(name = "loggingPolicy", category = "application", library = "kamon")
+ @Override
+ public SagaResponse apply(SagaTask task, SagaRequest request, SagaResponse parentResponse) {
+ log.info("Starting request id={} for service {}", request.id(), request.serviceName());
+ SagaResponse response = recoveryPolicy.apply(task, request, parentResponse);
+ log.info("Completed request id={} for service {}", request.id(), request.serviceName());
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/LongIdGenerator.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/LongIdGenerator.java
new file mode 100644
index 0000000..6b16075
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/LongIdGenerator.java
@@ -0,0 +1,30 @@
+/*
+ * 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.saga.core;
+
+import java.util.concurrent.atomic.AtomicLong;
+
+public class LongIdGenerator implements IdGenerator<Long> {
+
+ private final AtomicLong atomicLong = new AtomicLong();
+
+ @Override
+ public Long nextId() {
+ return atomicLong.incrementAndGet();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/NoOpSagaRequest.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/NoOpSagaRequest.java
new file mode 100644
index 0000000..56ad05e
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/NoOpSagaRequest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.saga.core;
+
+import static org.apache.servicecomb.saga.core.Compensation.SAGA_END_COMPENSATION;
+import static org.apache.servicecomb.saga.core.Compensation.SAGA_START_COMPENSATION;
+import static org.apache.servicecomb.saga.core.Operation.TYPE_NOP;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_END_TASK;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_START_TASK;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_END_TRANSACTION;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_START_TRANSACTION;
+
+import java.util.Arrays;
+
+public class NoOpSagaRequest implements SagaRequest {
+
+ public static final SagaRequest SAGA_START_REQUEST = new NoOpSagaRequest(
+ "saga-start",
+ SAGA_START_TRANSACTION,
+ SAGA_START_COMPENSATION,
+ SAGA_START_TASK);
+
+ public static final SagaRequest SAGA_END_REQUEST = new NoOpSagaRequest(
+ "saga-end",
+ SAGA_END_TRANSACTION,
+ SAGA_END_COMPENSATION,
+ SAGA_END_TASK);
+
+ private final String id;
+ private final Transaction transaction;
+ private final Compensation compensation;
+ private final String task;
+ private final String[] parents = {};
+
+ private NoOpSagaRequest(String id, Transaction transaction, Compensation compensation, String task) {
+ this.id = id;
+ this.transaction = transaction;
+ this.compensation = compensation;
+ this.task = task;
+ }
+
+ @Override
+ public Transaction transaction() {
+ return transaction;
+ }
+
+ @Override
+ public Compensation compensation() {
+ return compensation;
+ }
+
+ @Override
+ public String serviceName() {
+ return "Saga";
+ }
+
+ @Override
+ public String id() {
+ return id;
+ }
+
+ @Override
+ public String type() {
+ return TYPE_NOP;
+ }
+
+ @Override
+ public String task() {
+ return task;
+ }
+
+ @Override
+ public String[] parents() {
+ return parents;
+ }
+
+ @Override
+ public int failRetryDelayMilliseconds() {
+ return 50;
+ }
+
+ @Override
+ public String toString() {
+ return "NoOpSagaRequest{" +
+ "id='" + id + '\'' +
+ ", transaction=" + transaction +
+ ", compensation=" + compensation +
+ ", task='" + task + '\'' +
+ ", parents=" + Arrays.toString(parents) +
+ '}';
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Operation.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Operation.java
new file mode 100644
index 0000000..13b6f8a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Operation.java
@@ -0,0 +1,33 @@
+/*
+ * 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.saga.core;
+
+public interface Operation {
+
+ String TYPE_NOP = "NOP";
+ String TYPE_REST = "rest";
+ SagaResponse SUCCESSFUL_SAGA_RESPONSE = new SuccessfulSagaResponse("success");
+
+ default SagaResponse send(String address) {
+ return SUCCESSFUL_SAGA_RESPONSE;
+ }
+
+ default SagaResponse send(String address, SagaResponse response) {
+ return send(address);
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentLog.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentLog.java
new file mode 100644
index 0000000..26ec4bc
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentLog.java
@@ -0,0 +1,22 @@
+/*
+ * 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.saga.core;
+
+public interface PersistentLog {
+ void offer(SagaEvent sagaEvent);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentStore.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentStore.java
new file mode 100644
index 0000000..a7620b3
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/PersistentStore.java
@@ -0,0 +1,26 @@
+/*
+ * 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.saga.core;
+
+import java.util.List;
+import java.util.Map;
+
+public interface PersistentStore extends PersistentLog {
+
+ Map<String, List<EventEnvelope>> findPendingSagaEvents();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/RecoveryPolicy.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RecoveryPolicy.java
new file mode 100644
index 0000000..c67d640
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RecoveryPolicy.java
@@ -0,0 +1,26 @@
+/*
+ * 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.saga.core;
+
+public interface RecoveryPolicy extends Descriptive {
+
+ String SAGA_FORWARD_RECOVERY_POLICY = "ForwardRecovery";
+ String SAGA_BACKWARD_RECOVERY_POLICY = "BackwardRecovery";
+
+ SagaResponse apply(SagaTask task, SagaRequest request, SagaResponse parentResponse);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/RequestProcessTask.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RequestProcessTask.java
new file mode 100644
index 0000000..e19c7bf
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RequestProcessTask.java
@@ -0,0 +1,67 @@
+/*
+ * 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.saga.core;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class RequestProcessTask implements SagaTask {
+
+ private final String sagaId;
+ private final SagaLog sagaLog;
+ private final RecoveryPolicy recoveryPolicy;
+ private final FallbackPolicy fallbackPolicy;
+
+ public RequestProcessTask(
+ String sagaId,
+ SagaLog sagaLog,
+ RecoveryPolicy recoveryPolicy,
+ FallbackPolicy fallbackPolicy) {
+
+ this.sagaId = sagaId;
+ this.sagaLog = sagaLog;
+ this.recoveryPolicy = recoveryPolicy;
+ this.fallbackPolicy = fallbackPolicy;
+ }
+
+ @Segment(name = "commit", category = "application", library = "kamon")
+ @Override
+ public SagaResponse commit(SagaRequest request, SagaResponse parentResponse) {
+ sagaLog.offer(new TransactionStartedEvent(sagaId, request));
+
+ SagaResponse response = recoveryPolicy.apply(this, request, parentResponse);
+
+ sagaLog.offer(new TransactionEndedEvent(sagaId, request, response));
+ return response;
+ }
+
+ @Segment(name = "compensate", category = "application", library = "kamon")
+ @Override
+ public void compensate(SagaRequest request) {
+ Compensation compensation = request.compensation();
+ SagaResponse response = fallbackPolicy.apply(request.serviceName(), compensation, request.fallback());
+
+ sagaLog.offer(new TransactionCompensatedEvent(sagaId, request, response));
+ }
+
+ @Override
+ public void abort(SagaRequest request, Exception e) {
+ sagaLog.offer(new TransactionAbortedEvent(sagaId, request, e));
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/RestOperation.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RestOperation.java
new file mode 100644
index 0000000..5cce22d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/RestOperation.java
@@ -0,0 +1,59 @@
+/*
+ * 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.saga.core;
+
+import static java.util.Collections.emptyMap;
+
+import org.apache.servicecomb.saga.core.application.interpreter.RestRequestChecker;
+import java.util.Map;
+
+public class RestOperation implements Operation {
+
+ private final String path;
+ private final String method;
+ private final Map<String, Map<String, String>> params;
+
+ public RestOperation(String path, String method, Map<String, Map<String, String>> params) {
+ RestRequestChecker.checkParameters(method, params);
+
+ this.path = path;
+ this.method = method;
+ this.params = params == null? emptyMap() : params;
+ }
+
+ public String path() {
+ return path;
+ }
+
+ public String method() {
+ return method;
+ }
+
+ public Map<String, Map<String, String>> params() {
+ return params;
+ }
+
+ @Override
+ public String toString() {
+ return "Operation{" +
+ "path='" + path + '\'' +
+ ", method='" + method + '\'' +
+ ", params=" + params +
+ '}';
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Saga.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Saga.java
new file mode 100644
index 0000000..44f62ea
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Saga.java
@@ -0,0 +1,27 @@
+/*
+ * 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.saga.core;
+
+import kamon.annotation.Segment;
+
+public interface Saga {
+ @Segment(name = "runSaga", category = "application", library = "kamon")
+ SagaResponse run();
+
+ void play();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContext.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContext.java
new file mode 100644
index 0000000..309be44
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContext.java
@@ -0,0 +1,36 @@
+/*
+ * 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.saga.core;
+
+import java.util.function.Consumer;
+
+public interface SagaContext extends EventContext {
+ boolean isCompensationStarted();
+
+ boolean isTransactionCompleted(SagaRequest request);
+
+ boolean isCompensationCompleted(SagaRequest request);
+
+ void handleHangingTransactions(Consumer<SagaRequest> consumer);
+
+ SagaResponse responseOf(String requestId);
+
+ boolean isChosenChild(SagaRequest request);
+
+ SagaResponse responseOf(String[] parentRequestIds);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContextImpl.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContextImpl.java
new file mode 100644
index 0000000..610ae27
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaContextImpl.java
@@ -0,0 +1,133 @@
+/*
+ * 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.saga.core;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentSkipListSet;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+
+public class SagaContextImpl implements SagaContext {
+
+ private final Map<String, SagaResponse> completedTransactions;
+ private final Map<String, SagaResponse> completedCompensations;
+ private final Set<String> abortedTransactions;
+ private final Map<String, SagaRequest> hangingTransactions;
+ private final FromJsonFormat<Set<String>> childrenExtractor;
+
+ public SagaContextImpl(FromJsonFormat<Set<String>> childrenExtractor) {
+ this.childrenExtractor = childrenExtractor;
+ this.completedTransactions = new ConcurrentHashMap<>();
+ this.completedCompensations = new HashMap<>();
+ this.abortedTransactions = new ConcurrentSkipListSet<>();
+ this.hangingTransactions = new ConcurrentHashMap<>();
+ }
+
+ @Override
+ public boolean isCompensationStarted() {
+ return !abortedTransactions.isEmpty() || !completedCompensations.isEmpty();
+ }
+
+ @Override
+ public boolean isTransactionCompleted(SagaRequest request) {
+ return completedTransactions.containsKey(request.id());
+ }
+
+ @Override
+ public boolean isCompensationCompleted(SagaRequest request) {
+ return completedCompensations.containsKey(request.id());
+ }
+
+ @Override
+ public void beginTransaction(SagaRequest request) {
+ hangingTransactions.put(request.id(), request);
+ }
+
+ @Override
+ public void endTransaction(SagaRequest request, SagaResponse response) {
+ completedTransactions.put(request.id(), response);
+ hangingTransactions.remove(request.id());
+ }
+
+ @Override
+ public void abortTransaction(SagaRequest request, SagaResponse response) {
+ completedTransactions.remove(request.id());
+ abortedTransactions.add(request.id());
+ hangingTransactions.remove(request.id());
+ }
+
+ @Override
+ public void compensateTransaction(SagaRequest request, SagaResponse response) {
+ completedCompensations.put(request.id(), response);
+ completedTransactions.remove(request.id());
+ }
+
+ @Override
+ public void handleHangingTransactions(Consumer<SagaRequest> consumer) {
+ for (Iterator<SagaRequest> iterator = hangingTransactions.values().iterator(); iterator.hasNext(); ) {
+ consumer.accept(iterator.next());
+ }
+ }
+
+ @Override
+ public SagaResponse responseOf(String requestId) {
+ return completedTransactions.getOrDefault(requestId, SagaResponse.NONE_RESPONSE);
+ }
+
+ private List<SagaResponse> responsesOf(String[] parentRequestIds) {
+ return Arrays.stream(parentRequestIds)
+ .map(this::responseOf)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public SagaResponse responseOf(String[] parentRequestIds) {
+ List<SagaResponse> responses = responsesOf(parentRequestIds);
+
+ if (responses.isEmpty()) {
+ return SagaResponse.EMPTY_RESPONSE;
+ }
+
+ if (responses.size() == 1) {
+ return responses.get(0);
+ }
+
+ return new CompositeSagaResponse(responses);
+ }
+
+ @Override
+ public boolean isChosenChild(SagaRequest request) {
+ Set<String> chosenChildren = chosenChildrenOf(request.parents());
+ return chosenChildren.isEmpty() || chosenChildren.contains(request.id());
+ }
+
+ private Set<String> chosenChildrenOf(String[] parentRequestIds) {
+ return Arrays.stream(parentRequestIds)
+ .map(this::responseOf)
+ .flatMap(sagaResponse -> childrenExtractor.fromJson(sagaResponse.body()).stream())
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaDefinition.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaDefinition.java
new file mode 100644
index 0000000..406c8df
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaDefinition.java
@@ -0,0 +1,25 @@
+/*
+ * 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.saga.core;
+
+public interface SagaDefinition {
+
+ RecoveryPolicy policy();
+
+ SagaRequest[] requests();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndTask.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndTask.java
new file mode 100644
index 0000000..69daba7
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndTask.java
@@ -0,0 +1,48 @@
+/*
+ * 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.saga.core;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class SagaEndTask implements SagaTask {
+
+ private final String sagaId;
+ private final SagaLog sagaLog;
+
+ public SagaEndTask(String sagaId, SagaLog sagaLog) {
+ this.sagaId = sagaId;
+ this.sagaLog = sagaLog;
+ }
+
+ @Segment(name = "endTaskCommit", category = "application", library = "kamon")
+ @Override
+ public SagaResponse commit(SagaRequest request, SagaResponse parentResponse) {
+ sagaLog.offer(new SagaEndedEvent(sagaId, request));
+ return SagaResponse.EMPTY_RESPONSE;
+ }
+
+ @Override
+ public void compensate(SagaRequest request) {
+ }
+
+ @Override
+ public void abort(SagaRequest request, Exception e) {
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndedEvent.java
new file mode 100644
index 0000000..c6f4f9f
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEndedEvent.java
@@ -0,0 +1,38 @@
+/*
+ * 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.saga.core;
+
+public class SagaEndedEvent extends SagaEvent {
+
+ public SagaEndedEvent(String sagaId, SagaRequest sagaTask) {
+ super(sagaId, sagaTask);
+ }
+
+ @Override
+ public void gatherTo(EventContext sagaContext) {
+ sagaContext.endTransaction(payload(), SagaResponse.EMPTY_RESPONSE);
+ }
+
+ @Override
+ public String toString() {
+ return "SagaEndedEvent{id="
+ + payload().id()
+ + ", sagaId=" + sagaId
+ + "}";
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEvent.java
new file mode 100644
index 0000000..ceb0ebe
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaEvent.java
@@ -0,0 +1,39 @@
+/*
+ * 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.saga.core;
+
+public abstract class SagaEvent implements Descriptive {
+
+ public final String sagaId;
+ private final SagaRequest payload;
+
+ public SagaEvent(String sagaId, SagaRequest payload) {
+ this.sagaId = sagaId;
+ this.payload = payload;
+ }
+
+ public SagaRequest payload() {
+ return payload;
+ }
+
+ public abstract void gatherTo(EventContext sagaContext);
+
+ public String json(ToJsonFormat toJsonFormat) {
+ return "{}";
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaException.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaException.java
new file mode 100644
index 0000000..49ce989
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.saga.core;
+
+public class SagaException extends RuntimeException {
+
+ public SagaException(String cause, Throwable e) {
+ super(cause, e);
+ }
+
+ public SagaException(String cause) {
+ super(cause);
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaLog.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaLog.java
new file mode 100644
index 0000000..2e07b9f
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaLog.java
@@ -0,0 +1,22 @@
+/*
+ * 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.saga.core;
+
+public interface SagaLog extends PersistentLog {
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequest.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequest.java
new file mode 100644
index 0000000..337abdd
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequest.java
@@ -0,0 +1,48 @@
+/*
+ * 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.saga.core;
+
+import static org.apache.servicecomb.saga.core.Fallback.NOP_FALLBACK;
+
+public interface SagaRequest {
+
+ String PARAM_FORM = "form";
+ String PARAM_JSON = "json";
+ String PARAM_JSON_BODY = "body";
+ String PARAM_QUERY = "query";
+
+ Transaction transaction();
+
+ Compensation compensation();
+
+ default Fallback fallback() {
+ return NOP_FALLBACK;
+ }
+
+ String serviceName();
+
+ String id();
+
+ String type();
+
+ String task();
+
+ String[] parents();
+
+ int failRetryDelayMilliseconds();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequestImpl.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequestImpl.java
new file mode 100644
index 0000000..cbb7a23
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaRequestImpl.java
@@ -0,0 +1,153 @@
+/*
+ * 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.saga.core;
+
+import static org.apache.servicecomb.saga.core.Fallback.NOP_FALLBACK;
+
+import java.util.Arrays;
+
+public class SagaRequestImpl implements SagaRequest {
+
+ private final String id;
+ private final String serviceName;
+ private final String type;
+ private final Transaction transaction;
+ private final Compensation compensation;
+ private final String[] parents;
+ private final Fallback fallback;
+ private final int failRetryDelayMilliseconds;
+
+ public SagaRequestImpl(
+ String id,
+ String serviceName,
+ String type,
+ Transaction transaction,
+ Compensation compensation,
+ Fallback fallback,
+ String[] parents,
+ int failRetryDelayMilliseconds) {
+
+ this.id = id;
+ this.serviceName = serviceName;
+ this.type = type;
+ this.transaction = transaction;
+ this.compensation = compensation;
+ this.fallback = fallback;
+ this.failRetryDelayMilliseconds = failRetryDelayMilliseconds <= 0 ? 50 : failRetryDelayMilliseconds;
+ // TODO: 2017/10/21 set parent to root when null
+ this.parents = parents == null ? new String[0] : parents;
+ }
+
+ public SagaRequestImpl(
+ String id,
+ String serviceName,
+ String type,
+ Transaction transaction,
+ Compensation compensation,
+ Fallback fallback,
+ String[] parents) {
+ this(id, serviceName, type, transaction, compensation, fallback, parents, 0);
+ }
+
+ public SagaRequestImpl(
+ String id,
+ String serviceName,
+ String type,
+ Transaction transaction,
+ Compensation compensation,
+ Fallback fallback) {
+ this(id, serviceName, type, transaction, compensation, fallback, new String[0]);
+ }
+
+ public SagaRequestImpl(
+ String id,
+ String serviceName,
+ String type,
+ Transaction transaction,
+ Compensation compensation) {
+ this(id, serviceName, type, transaction, compensation, NOP_FALLBACK, new String[0]);
+ }
+
+ public SagaRequestImpl(
+ String id,
+ String serviceName,
+ String type,
+ Transaction transaction,
+ Compensation compensation,
+ String[] parents) {
+ this(id, serviceName, type, transaction, compensation, NOP_FALLBACK, parents);
+ }
+
+ @Override
+ public Transaction transaction() {
+ return transaction;
+ }
+
+ @Override
+ public Compensation compensation() {
+ return compensation;
+ }
+
+ @Override
+ public Fallback fallback() {
+ return fallback;
+ }
+
+ @Override
+ public String serviceName() {
+ return serviceName;
+ }
+
+ @Override
+ public String id() {
+ return id;
+ }
+
+ @Override
+ public String type() {
+ return type;
+ }
+
+ @Override
+ public String task() {
+ return SagaTask.SAGA_REQUEST_TASK;
+ }
+
+ @Override
+ public String[] parents() {
+ return parents;
+ }
+
+ @Override
+ public int failRetryDelayMilliseconds() {
+ return failRetryDelayMilliseconds;
+ }
+
+ @Override
+ public String toString() {
+ return "SagaRequest{" +
+ "id='" + id + '\'' +
+ ", serviceName='" + serviceName + '\'' +
+ ", type='" + type + '\'' +
+ ", transaction=" + transaction +
+ ", compensation=" + compensation +
+ ", fallback=" + fallback +
+ ", parents=" + Arrays.toString(parents) +
+ '}';
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaResponse.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaResponse.java
new file mode 100644
index 0000000..ecdfe4a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaResponse.java
@@ -0,0 +1,61 @@
+/*
+ * 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.saga.core;
+
+public interface SagaResponse {
+
+ SagaResponse EMPTY_RESPONSE = new SagaResponse() {
+ @Override
+ public boolean succeeded() {
+ return true;
+ }
+
+ @Override
+ public String body() {
+ return "{}";
+ }
+
+ @Override
+ public String toString() {
+ return "EMPTY_RESPONSE{body={}}";
+ }
+ };
+
+ SagaResponse NONE_RESPONSE = new SagaResponse() {
+ @Override
+ public boolean succeeded() {
+ return false;
+ }
+
+ @Override
+ public String body() {
+ return "{\n"
+ + " \"sagaChildren\": [\"none\"]\n"
+ + "}";
+ }
+
+ @Override
+ public String toString() {
+ return "NONE_RESPONSE{body={\"sagaChildren\": [\"none\"]\n}}";
+ }
+ };
+
+ boolean succeeded();
+
+ String body();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartFailedException.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartFailedException.java
new file mode 100644
index 0000000..0de35da
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartFailedException.java
@@ -0,0 +1,25 @@
+/*
+ * 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.saga.core;
+
+public class SagaStartFailedException extends RuntimeException {
+
+ public SagaStartFailedException(String cause, Throwable e) {
+ super(cause, e);
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartTask.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartTask.java
new file mode 100644
index 0000000..d8a6bd1
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartTask.java
@@ -0,0 +1,56 @@
+/*
+ * 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.saga.core;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class SagaStartTask implements SagaTask {
+
+ private final String sagaId;
+ private final String requestJson;
+ private final SagaLog sagaLog;
+
+ public SagaStartTask(String sagaId, String requestJson, SagaLog sagaLog) {
+ this.sagaId = sagaId;
+ this.requestJson = requestJson;
+ this.sagaLog = sagaLog;
+ }
+
+ @Segment(name = "startTaskCommit", category = "application", library = "kamon")
+ @Override
+ public SagaResponse commit(SagaRequest request, SagaResponse parentResponse) {
+ try {
+ sagaLog.offer(new SagaStartedEvent(sagaId, requestJson, request));
+ } catch (Exception e) {
+ throw new SagaStartFailedException("Failed to persist SagaStartedEvent for " + requestJson, e);
+ }
+ return SagaResponse.EMPTY_RESPONSE;
+ }
+
+ @Override
+ public void compensate(SagaRequest request) {
+ sagaLog.offer(new SagaEndedEvent(sagaId, request));
+ }
+
+ @Override
+ public void abort(SagaRequest request, Exception e) {
+
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartedEvent.java
new file mode 100644
index 0000000..0d82fee
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaStartedEvent.java
@@ -0,0 +1,46 @@
+/*
+ * 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.saga.core;
+
+public class SagaStartedEvent extends SagaEvent {
+
+ private final String requestJson;
+
+ public SagaStartedEvent(String sagaId, String requestJson, SagaRequest request) {
+ super(sagaId, request);
+ this.requestJson = requestJson;
+ }
+
+ @Override
+ public void gatherTo(EventContext sagaContext) {
+ sagaContext.endTransaction(payload(), SagaResponse.EMPTY_RESPONSE);
+ }
+
+ @Override
+ public String json(ToJsonFormat toJsonFormat) {
+ return requestJson;
+ }
+
+ @Override
+ public String toString() {
+ return "SagaStartedEvent{id="
+ + payload().id()
+ + ", sagaId=" + sagaId
+ + "}";
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaState.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaState.java
new file mode 100644
index 0000000..eae3678
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaState.java
@@ -0,0 +1,27 @@
+/*
+ * 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.saga.core;
+
+interface SagaState {
+
+ boolean hasNext();
+
+ void run();
+
+ void replay();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTask.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTask.java
new file mode 100644
index 0000000..73efcc0
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTask.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.saga.core;
+
+public interface SagaTask {
+
+ String SAGA_START_TASK = "SagaStart";
+ String SAGA_REQUEST_TASK = "SagaRequest";
+ String SAGA_END_TASK = "SagaEnd";
+
+ SagaResponse commit(SagaRequest request, SagaResponse parentResponse);
+
+ void compensate(SagaRequest request);
+
+ void abort(SagaRequest request, Exception e);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTaskFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTaskFactory.java
new file mode 100644
index 0000000..0fd2450
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SagaTaskFactory.java
@@ -0,0 +1,98 @@
+/*
+ * 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.saga.core;
+
+import java.lang.invoke.MethodHandles;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class SagaTaskFactory {
+ private final FallbackPolicy fallbackPolicy;
+ private final RetrySagaLog retrySagaLog;
+ private final PersistentStore persistentStore;
+
+ public SagaTaskFactory(int retryDelay, PersistentStore persistentStore) {
+ this.persistentStore = persistentStore;
+
+ fallbackPolicy = new FallbackPolicy(retryDelay);
+ retrySagaLog = new RetrySagaLog(persistentStore, retryDelay);
+ }
+
+ public Map<String, SagaTask> sagaTasks(String sagaId,
+ String requestJson,
+ RecoveryPolicy recoveryPolicy,
+ EventStore sagaLog) {
+
+ SagaLog compositeSagaLog = compositeSagaLog(sagaLog, persistentStore);
+
+ return new HashMap<String, SagaTask>() {{
+ put(SagaTask.SAGA_START_TASK, new SagaStartTask(sagaId, requestJson, compositeSagaLog));
+
+ SagaLog retrySagaLog = compositeSagaLog(sagaLog, SagaTaskFactory.this.retrySagaLog);
+ put(SagaTask.SAGA_REQUEST_TASK,
+ new RequestProcessTask(sagaId, retrySagaLog, new LoggingRecoveryPolicy(recoveryPolicy), fallbackPolicy));
+ put(SagaTask.SAGA_END_TASK, new SagaEndTask(sagaId, retrySagaLog));
+ }};
+ }
+
+ private CompositeSagaLog compositeSagaLog(SagaLog sagaLog, PersistentLog persistentLog) {
+ return new CompositeSagaLog(sagaLog, persistentLog);
+ }
+
+ static class RetrySagaLog implements PersistentLog {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ private final PersistentStore persistentStore;
+ private final int retryDelay;
+
+ RetrySagaLog(PersistentStore persistentStore, int retryDelay) {
+ this.persistentStore = persistentStore;
+ this.retryDelay = retryDelay;
+ }
+
+ @Override
+ public void offer(SagaEvent sagaEvent) {
+ boolean success = false;
+ do {
+ try {
+ persistentStore.offer(sagaEvent);
+ success = true;
+ log.info("Persisted saga event {} successfully", sagaEvent);
+ } catch (Exception e) {
+ log.error("Failed to persist saga event {}", sagaEvent, e);
+ sleep(retryDelay);
+ }
+ } while (!success && !isInterrupted());
+ }
+
+ private boolean isInterrupted() {
+ return Thread.currentThread().isInterrupted();
+ }
+
+ private void sleep(int delay) {
+ try {
+ Thread.sleep(delay);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/SuccessfulSagaResponse.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SuccessfulSagaResponse.java
new file mode 100644
index 0000000..d69bdcd
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/SuccessfulSagaResponse.java
@@ -0,0 +1,43 @@
+/*
+ * 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.saga.core;
+
+public class SuccessfulSagaResponse implements SagaResponse {
+ private final String body;
+
+ public SuccessfulSagaResponse(String body) {
+ this.body = body;
+ }
+
+ @Override
+ public boolean succeeded() {
+ return true;
+ }
+
+ @Override
+ public String body() {
+ return body;
+ }
+
+ @Override
+ public String toString() {
+ return "SuccessfulSagaResponse{" +
+ "body='" + body + '\'' +
+ '}';
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskConsumer.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskConsumer.java
new file mode 100644
index 0000000..5ae7494
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskConsumer.java
@@ -0,0 +1,29 @@
+/*
+ * 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.saga.core;
+
+import java.util.Collection;
+
+import org.apache.servicecomb.saga.core.dag.Node;
+
+interface TaskConsumer {
+
+ void consume(Collection<Node<SagaRequest>> nodes);
+
+ boolean replay(Collection<Node<SagaRequest>> nodes);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskRunner.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskRunner.java
new file mode 100644
index 0000000..c078001
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TaskRunner.java
@@ -0,0 +1,72 @@
+/*
+ * 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.saga.core;
+
+import org.apache.servicecomb.saga.core.dag.Node;
+import org.apache.servicecomb.saga.core.dag.Traveller;
+
+import java.util.Collection;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+class TaskRunner implements SagaState {
+
+ private final Traveller<SagaRequest> traveller;
+ private final TaskConsumer taskConsumer;
+
+ TaskRunner(Traveller<SagaRequest> traveller, TaskConsumer taskConsumer) {
+ this.traveller = traveller;
+ this.taskConsumer = taskConsumer;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return traveller.hasNext();
+ }
+
+ @Segment(name = "runTask", category = "application", library = "kamon")
+ @Override
+ public void run() {
+ Collection<Node<SagaRequest>> nodes = traveller.nodes();
+
+ // finish pending tasks from saga log at startup
+ if (!nodes.isEmpty()) {
+ taskConsumer.consume(nodes);
+ nodes.clear();
+ }
+
+ while (traveller.hasNext()) {
+ traveller.next();
+ taskConsumer.consume(nodes);
+ nodes.clear();
+ }
+ }
+
+ @Override
+ public void replay() {
+ boolean played = false;
+ Collection<Node<SagaRequest>> nodes = traveller.nodes();
+
+ while (traveller.hasNext() && !played) {
+ traveller.next();
+ played = taskConsumer.replay(nodes);
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/ToJsonFormat.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/ToJsonFormat.java
new file mode 100644
index 0000000..cd6c449
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/ToJsonFormat.java
@@ -0,0 +1,26 @@
+/*
+ * 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.saga.core;
+
+public interface ToJsonFormat {
+
+ String toJson(SagaRequest request);
+
+ String toJson(SagaRequest request, SagaResponse response);
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transaction.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transaction.java
new file mode 100644
index 0000000..fafb845
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transaction.java
@@ -0,0 +1,27 @@
+/*
+ * 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.saga.core;
+
+public interface Transaction extends Operation {
+
+ Transaction SAGA_START_TRANSACTION = new Transaction() {
+ };
+
+ Transaction SAGA_END_TRANSACTION = new Transaction() {
+ };
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionAbortedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionAbortedEvent.java
new file mode 100644
index 0000000..b117c94
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionAbortedEvent.java
@@ -0,0 +1,57 @@
+/*
+ * 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.saga.core;
+
+public class TransactionAbortedEvent extends SagaEvent {
+
+ private final SagaResponse response;
+
+ public TransactionAbortedEvent(String sagaId, SagaRequest payload, Exception exception) {
+ super(sagaId, payload);
+ this.response = new FailedSagaResponse(exception);
+ }
+
+ public TransactionAbortedEvent(String sagaId, SagaRequest payload, SagaResponse response) {
+ super(sagaId, payload);
+ this.response = response;
+ }
+
+ @Override
+ public void gatherTo(EventContext sagaContext) {
+ // remove from completed operations in order not to compensate it
+ sagaContext.abortTransaction(payload(), response);
+ }
+
+ @Override
+ public String json(ToJsonFormat toJsonFormat) {
+ return toJsonFormat.toJson(payload(), response);
+ }
+
+ @Override
+ public String toString() {
+ return "TransactionAbortedEvent{id="
+ + payload().id()
+ + ", operation="
+ + payload().compensation()
+ + "}";
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionCompensatedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionCompensatedEvent.java
new file mode 100644
index 0000000..d6ab298
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionCompensatedEvent.java
@@ -0,0 +1,56 @@
+/*
+ * 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.saga.core;
+
+public class TransactionCompensatedEvent extends SagaEvent {
+
+ private final SagaResponse response;
+
+ public TransactionCompensatedEvent(String sagaId, SagaRequest request) {
+ this(sagaId, request, SagaResponse.EMPTY_RESPONSE);
+ }
+
+ public TransactionCompensatedEvent(String sagaId, SagaRequest request, SagaResponse response) {
+ super(sagaId, request);
+ this.response = response;
+ }
+
+ @Override
+ public void gatherTo(EventContext sagaContext) {
+ sagaContext.compensateTransaction(payload(), response);
+ }
+
+ @Override
+ public String json(ToJsonFormat toJsonFormat) {
+ return toJsonFormat.toJson(payload(), response);
+ }
+
+ @Override
+ public String toString() {
+ return "TransactionCompensatedEvent{id="
+ + payload().id()
+ + ", sagaId=" + sagaId
+ + ", operation="
+ + payload().compensation()
+ + "}";
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionEndedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionEndedEvent.java
new file mode 100644
index 0000000..603c542
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionEndedEvent.java
@@ -0,0 +1,56 @@
+/*
+ * 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.saga.core;
+
+public class TransactionEndedEvent extends SagaEvent {
+
+ private final SagaResponse response;
+
+ public TransactionEndedEvent(String sagaId, SagaRequest request) {
+ this(sagaId, request, SagaResponse.EMPTY_RESPONSE);
+ }
+
+ public TransactionEndedEvent(String sagaId, SagaRequest request, SagaResponse response) {
+ super(sagaId, request);
+ this.response = response;
+ }
+
+ @Override
+ public void gatherTo(EventContext sagaContext) {
+ sagaContext.endTransaction(payload(), response);
+ }
+
+ @Override
+ public String toString() {
+ return "TransactionEndedEvent{id="
+ + payload().id()
+ + ", sagaId=" + sagaId
+ + ", operation="
+ + payload().transaction()
+ + "}";
+ }
+
+ @Override
+ public String json(ToJsonFormat toJsonFormat) {
+ return toJsonFormat.toJson(payload(), response);
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionFailedException.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionFailedException.java
new file mode 100644
index 0000000..b55e01a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionFailedException.java
@@ -0,0 +1,29 @@
+/*
+ * 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.saga.core;
+
+public class TransactionFailedException extends RuntimeException {
+
+ public TransactionFailedException(Throwable throwable) {
+ super(throwable);
+ }
+
+ public TransactionFailedException(String cause) {
+ super(cause);
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionStartedEvent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionStartedEvent.java
new file mode 100644
index 0000000..cc35589
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionStartedEvent.java
@@ -0,0 +1,45 @@
+/*
+ * 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.saga.core;
+
+public class TransactionStartedEvent extends SagaEvent {
+
+ public TransactionStartedEvent(String sagaId, SagaRequest transaction) {
+ super(sagaId, transaction);
+ }
+
+ @Override
+ public void gatherTo(EventContext sagaContext) {
+ sagaContext.beginTransaction(payload());
+ }
+
+ @Override
+ public String toString() {
+ return "TransactionStartedEvent{id="
+ + payload().id()
+ + ", sagaId=" + sagaId
+ + ", operation="
+ + payload().transaction()
+ + "}";
+ }
+
+ @Override
+ public String json(ToJsonFormat toJsonFormat) {
+ return toJsonFormat.toJson(payload());
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionTaskConsumer.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionTaskConsumer.java
new file mode 100644
index 0000000..af2dc74
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransactionTaskConsumer.java
@@ -0,0 +1,123 @@
+/*
+ * 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.saga.core;
+
+import org.apache.servicecomb.saga.core.dag.Node;
+import java.lang.invoke.MethodHandles;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletionService;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+import kamon.annotation.Trace;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@EnableKamon
+class TransactionTaskConsumer implements TaskConsumer {
+
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ private final Map<String, SagaTask> tasks;
+ private final SagaContext sagaContext;
+ private final CompletionService<Operation> executorService;
+
+ TransactionTaskConsumer(
+ Map<String, SagaTask> tasks,
+ SagaContext sagaContext,
+ CompletionService<Operation> executorService) {
+
+ this.tasks = tasks;
+ this.sagaContext = sagaContext;
+ this.executorService = executorService;
+ }
+
+ @Segment(name = "consumeTask", category = "application", library = "kamon")
+ @Override
+ public void consume(Collection<Node<SagaRequest>> nodes) {
+ List<Future<Operation>> futures = new ArrayList<>(nodes.size());
+ for (Node<SagaRequest> node : nodes) {
+ SagaRequest request = node.value();
+ if (sagaContext.isChosenChild(request)) {
+ futures.add(futureOf(request));
+ }
+ }
+
+ for (int i = 0; i < futures.size(); i++) {
+ try {
+ executorService.take().get();
+ } catch (ExecutionException e) {
+ if (e.getCause() instanceof SagaStartFailedException) {
+ throw ((SagaStartFailedException) e.getCause());
+ }
+ throw new TransactionFailedException(e.getCause());
+ } catch (InterruptedException e) {
+ // TODO: 7/29/2017 what shall we do when system is shutting down?
+ throw new TransactionFailedException(e);
+ }
+ }
+ }
+
+ @Override
+ public boolean replay(Collection<Node<SagaRequest>> nodes) {
+ for (Iterator<Node<SagaRequest>> iterator = nodes.iterator(); iterator.hasNext(); ) {
+ SagaRequest request = iterator.next().value();
+ if (sagaContext.isTransactionCompleted(request)) {
+ log.info("Skipped completed transaction id={} operation={} while replay", request.id(), request.transaction());
+ iterator.remove();
+ }
+ }
+ return !nodes.isEmpty();
+ }
+
+ @Segment(name = "submitCallable", category = "application", library = "kamon")
+ private Future<Operation> futureOf(SagaRequest request) {
+ return executorService.submit(new OperationCallable(tasks, request, sagaContext.responseOf(request.parents())));
+ }
+
+ @EnableKamon
+ private static class OperationCallable implements Callable<Operation> {
+
+ private final SagaRequest request;
+ private final Map<String, SagaTask> tasks;
+ private final SagaResponse parentResponse;
+
+ private OperationCallable(
+ Map<String, SagaTask> tasks,
+ SagaRequest request,
+ SagaResponse parentResponse) {
+ this.request = request;
+ this.tasks = tasks;
+ this.parentResponse = parentResponse;
+ }
+
+ @Trace("runTransactionCallable")
+ @Override
+ public Operation call() throws Exception {
+ tasks.get(request.task()).commit(request, parentResponse);
+ return request.transaction();
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transport.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transport.java
new file mode 100644
index 0000000..5679e0a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/Transport.java
@@ -0,0 +1,22 @@
+/*
+ * 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.saga.core;
+
+public interface Transport {
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransportFailedException.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransportFailedException.java
new file mode 100644
index 0000000..e01a9d5
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/TransportFailedException.java
@@ -0,0 +1,28 @@
+/*
+ * 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.saga.core;
+
+public class TransportFailedException extends RuntimeException {
+ public TransportFailedException(String cause) {
+ super(cause);
+ }
+
+ public TransportFailedException(String cause, Throwable e) {
+ super(cause, e);
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSaga.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSaga.java
new file mode 100644
index 0000000..bf60f98
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSaga.java
@@ -0,0 +1,65 @@
+/*
+ * 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.saga.core.actors;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.servicecomb.saga.core.EventContext;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import org.apache.servicecomb.saga.core.SagaEvent;
+
+import akka.actor.ActorRef;
+
+public class ActorBasedSaga implements Saga {
+ private final ActorRef root;
+ private final ActorRef completionCallback;
+ private final CompletableFuture<SagaResponse> future;
+ private final EventStore sagaLog;
+ private final EventContext sagaContext;
+
+ ActorBasedSaga(ActorRef root, ActorRef completionCallback, CompletableFuture<SagaResponse> future, EventStore sagaLog,
+ EventContext sagaContext) {
+ this.root = root;
+ this.completionCallback = completionCallback;
+ this.future = future;
+ this.sagaLog = sagaLog;
+ this.sagaContext = sagaContext;
+ }
+
+ @Override
+ public SagaResponse run() {
+ root.tell(new TransactMessage(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE), completionCallback);
+
+ return future.join();
+ }
+
+ @Override
+ public void play() {
+ gatherEvents(sagaLog);
+ }
+
+ private void gatherEvents(Iterable<SagaEvent> events) {
+ for (SagaEvent event : events) {
+ event.gatherTo(sagaContext);
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaFactory.java
new file mode 100644
index 0000000..8ce22fa
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaFactory.java
@@ -0,0 +1,83 @@
+/*
+ * 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.saga.core.actors;
+
+import static akka.actor.ActorRef.noSender;
+
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+import org.apache.servicecomb.saga.core.SagaTaskFactory;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import scala.concurrent.Await;
+import scala.concurrent.duration.Duration;
+
+public class ActorBasedSagaFactory implements SagaFactory {
+ private final ActorSystem actorSystem = ActorSystem.create("saga");
+ private final RequestActorBuilder actorBuilder;
+ private final SagaTaskFactory sagaTaskFactory;
+
+ public ActorBasedSagaFactory(int retryDelay,
+ PersistentStore persistentStore,
+ FromJsonFormat<Set<String>> childrenExtractor) {
+
+ this.sagaTaskFactory = new SagaTaskFactory(retryDelay, persistentStore);
+ this.actorBuilder = new RequestActorBuilder(actorSystem, childrenExtractor);
+ }
+
+ @Override
+ public ActorBasedSaga createSaga(String requestJson, String sagaId, EventStore sagaLog, SagaDefinition definition) {
+
+ CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+ ActorRef completionCallback = actorSystem.actorOf(CompletionCallbackActor.props(future));
+ RequestActorContext context = actorBuilder.build(
+ definition.requests(),
+ sagaTaskFactory.sagaTasks(sagaId,
+ requestJson,
+ definition.policy(),
+ sagaLog
+ ),
+ completionCallback);
+
+ completionCallback.tell(context, noSender());
+ return new ActorBasedSaga(
+ context.actorOf(NoOpSagaRequest.SAGA_START_REQUEST.id()),
+ completionCallback,
+ future,
+ sagaLog,
+ new EventContextImpl(context));
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return actorSystem.whenTerminated().isCompleted();
+ }
+
+ @Override
+ public void terminate() throws Exception {
+ Await.result(actorSystem.terminate(), Duration.Inf());
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActor.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActor.java
new file mode 100644
index 0000000..7cee4d3
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActor.java
@@ -0,0 +1,69 @@
+/*
+ * 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.saga.core.actors;
+
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.AbortMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+import org.apache.servicecomb.saga.core.actors.messages.FailMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import akka.actor.AbstractLoggingActor;
+import akka.actor.Props;
+
+class CompletionCallbackActor extends AbstractLoggingActor {
+ private final CompletableFuture<SagaResponse> future;
+
+ private CompletionCallbackActor(CompletableFuture<SagaResponse> future) {
+ this.future = future;
+ }
+
+ static Props props(CompletableFuture<SagaResponse> future) {
+ return Props.create(CompletionCallbackActor.class, () -> new CompletionCallbackActor(future));
+ }
+
+ @Override
+ public Receive createReceive() {
+ return receiveBuilder()
+ .match(RequestActorContext.class, this::ready)
+ .build();
+ }
+
+ private void ready(RequestActorContext context) {
+ getContext().become(receiveBuilder()
+ .match(CompensateMessage.class, message -> end(context, message.response()))
+ .match(TransactMessage.class, message -> end(context, message.response()))
+ .match(AbortMessage.class, message -> onAbort(context, message))
+ .match(FailMessage.class, message -> end(context, message.response()))
+ .build());
+ }
+
+ private void onAbort(RequestActorContext context, AbortMessage message) {
+ log().info("saga actor: received abort message of {}", message.response());
+ context.actorOf(NoOpSagaRequest.SAGA_END_REQUEST.id()).tell(new CompensateMessage(message.response()), self());
+ }
+
+ private void end(RequestActorContext context, SagaResponse response) {
+ log().info("saga actor: received response {}", response);
+ future.complete(response);
+ context.forAll(actor -> getContext().stop(actor));
+ getContext().stop(self());
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/EventContextImpl.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/EventContextImpl.java
new file mode 100644
index 0000000..21a2928
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/EventContextImpl.java
@@ -0,0 +1,60 @@
+/*
+ * 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.saga.core.actors;
+
+import static akka.actor.ActorRef.noSender;
+
+import org.apache.servicecomb.saga.core.EventContext;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.AbortRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensationRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactionRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.Message;
+
+public class EventContextImpl implements EventContext {
+ private final RequestActorContext context;
+
+ EventContextImpl(RequestActorContext context) {
+ this.context = context;
+ }
+
+ @Override
+ public void beginTransaction(SagaRequest request) {
+
+ }
+
+ @Override
+ public void endTransaction(SagaRequest request, SagaResponse response) {
+ sendMessage(request, new TransactionRecoveryMessage(response));
+ }
+
+ @Override
+ public void abortTransaction(SagaRequest request, SagaResponse response) {
+ sendMessage(request, new AbortRecoveryMessage(response));
+ }
+
+ @Override
+ public void compensateTransaction(SagaRequest request, SagaResponse response) {
+ sendMessage(request, new CompensationRecoveryMessage());
+ }
+
+ private void sendMessage(SagaRequest request, Message message) {
+ context.actorOf(request.id()).tell(message, noSender());
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActor.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActor.java
new file mode 100644
index 0000000..055174e
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActor.java
@@ -0,0 +1,206 @@
+/*
+ * 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.saga.core.actors;
+
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
+import org.apache.servicecomb.saga.core.CompositeSagaResponse;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.SagaStartFailedException;
+import org.apache.servicecomb.saga.core.SagaTask;
+import org.apache.servicecomb.saga.core.TransactionFailedException;
+import org.apache.servicecomb.saga.core.actors.messages.AbortMessage;
+import org.apache.servicecomb.saga.core.actors.messages.AbortRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensationRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.FailMessage;
+import org.apache.servicecomb.saga.core.actors.messages.Message;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactionRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+
+import akka.actor.AbstractLoggingActor;
+import akka.actor.ActorRef;
+import akka.actor.Props;
+import akka.japi.pf.ReceiveBuilder;
+
+public class RequestActor extends AbstractLoggingActor {
+ private final RequestActorContext context;
+ private final SagaTask task;
+ private final SagaRequest request;
+
+ private final List<SagaResponse> parentResponses;
+ private final List<ActorRef> compensatedChildren;
+
+ private final Receive transacted;
+ private final Receive aborted;
+
+ static Props props(
+ RequestActorContext context,
+ SagaTask task,
+ SagaRequest request) {
+ return Props.create(RequestActor.class, () -> new RequestActor(context, task, request));
+ }
+
+ private RequestActor(
+ RequestActorContext context,
+ SagaTask task,
+ SagaRequest request) {
+ this.context = context;
+ this.task = task;
+ this.request = request;
+ this.parentResponses = new ArrayList<>(request.parents().length);
+ this.compensatedChildren = new LinkedList<>();
+
+ this.aborted = onReceive(ignored -> {
+ }).build();
+
+ this.transacted = onReceive(task::compensate)
+ .match(CompensationRecoveryMessage.class, message -> getContext().become(aborted))
+ .build();
+ }
+
+ @Override
+ public Receive createReceive() {
+ return receiveBuilder()
+ .match(TransactMessage.class,
+ message -> onTransaction(message, () -> task.commit(request, responseOf(parentResponses))))
+ .match(TransactionRecoveryMessage.class, this::onTransactRecovery)
+ .match(AbortRecoveryMessage.class, this::onAbortRecovery)
+ .match(AbortMessage.class, this::onAbort)
+ .build();
+ }
+
+ private void onAbort(AbortMessage message) {
+ log().debug("{}: received abort message of {}", request.id(), message.response());
+ sendToChildrenButSender(message);
+ sendToParentsButSender(message);
+
+ getContext().become(aborted);
+ }
+
+ private void sendToParentsButSender(AbortMessage message) {
+ context.parentsOf(request)
+ .stream()
+ .filter(this::isNotSender)
+ .forEach(actor -> actor.tell(message, self()));
+ }
+
+ private void sendToChildrenButSender(AbortMessage message) {
+ context.childrenOf(request)
+ .stream()
+ .filter(this::isNotSender)
+ .forEach(actor -> actor.tell(message, self()));
+ }
+
+ private boolean isNotSender(ActorRef actor) {
+ return !actor.equals(sender());
+ }
+
+ private void onTransactRecovery(TransactionRecoveryMessage message) {
+ getContext().become(receiveBuilder()
+ .match(TransactMessage.class, m -> onTransaction(m, message::response))
+ .match(CompensationRecoveryMessage.class, m -> getContext().become(aborted))
+ .build()
+ );
+ }
+
+ private void onAbortRecovery(AbortRecoveryMessage message) {
+ getContext().become(
+ receiveBuilder()
+ .match(TransactMessage.class, m -> onAbort(new AbortMessage(message.response())))
+ .build());
+ }
+
+ private void onTransaction(TransactMessage message, Supplier<SagaResponse> responseSupplier) {
+ log().debug("{}: received transaction message of {}", request.id(), message.request());
+ if (context.parentsOf(request).contains(sender())) {
+ parentResponses.add(message.response());
+ }
+
+ if (parentResponses.size() == context.parentsOf(request).size()) {
+ transact(responseSupplier);
+ }
+ }
+
+ private void transact(Supplier<SagaResponse> responseSupplier) {
+ try {
+ if (isChosenChild(parentResponses)) {
+ SagaResponse sagaResponse = responseSupplier.get();
+ sendToChildren(new TransactMessage(request, sagaResponse));
+ getContext().become(transacted);
+ } else {
+ sendToChildren(new TransactMessage(request, SagaResponse.NONE_RESPONSE));
+ getContext().become(aborted);
+ }
+ } catch (SagaStartFailedException e) {
+ sendToParents(new FailMessage(e));
+ } catch (Exception e) {
+ log().error("Failed to run operation {} with error {}", request.transaction(), e);
+
+ Message abortMessage = new AbortMessage(new TransactionFailedException(e));
+ sendToParents(abortMessage);
+ sendToChildren(abortMessage);
+ getContext().become(aborted);
+ }
+ }
+
+ private void sendToParents(Message message) {
+ context.parentsOf(request).forEach(actor -> actor.tell(message, self()));
+ }
+
+ private void sendToChildren(Message message) {
+ context.childrenOf(request).forEach(actor -> actor.tell(message, self()));
+ }
+
+ private boolean isChosenChild(List<SagaResponse> parentResponses) {
+ return request.parents().length == 0 || parentResponses.isEmpty() || parentResponses.stream()
+ .map(context::chosenChildren)
+ .anyMatch(chosenChildren -> chosenChildren.isEmpty() || chosenChildren.contains(request.id()));
+ }
+
+ private SagaResponse responseOf(List<SagaResponse> responseContexts) {
+ if (responseContexts.isEmpty()) {
+ return SagaResponse.EMPTY_RESPONSE;
+ }
+
+ if (responseContexts.size() == 1) {
+ return responseContexts.get(0);
+ }
+ return new CompositeSagaResponse(responseContexts);
+ }
+
+ private ReceiveBuilder onReceive(Consumer<SagaRequest> requestConsumer) {
+ return receiveBuilder()
+ .match(CompensateMessage.class, message -> onCompensate(message, requestConsumer));
+ }
+
+ private void onCompensate(CompensateMessage message, Consumer<SagaRequest> requestConsumer) {
+ log().debug("{}: received compensation message from {}", request.id(), sender());
+ compensatedChildren.add(sender());
+
+ if (compensatedChildren.size() == context.childrenOf(request).size()) {
+ requestConsumer.accept(request);
+ sendToParents(message);
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilder.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilder.java
new file mode 100644
index 0000000..5004b4e
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilder.java
@@ -0,0 +1,108 @@
+/*
+ * 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.saga.core.actors;
+
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaTask;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.actor.Props;
+
+public class RequestActorBuilder {
+ private final ActorSystem actorSystem;
+ private final FromJsonFormat<Set<String>> childrenExtractor;
+
+ RequestActorBuilder(
+ ActorSystem actorSystem,
+ FromJsonFormat<Set<String>> childrenExtractor) {
+
+ this.actorSystem = actorSystem;
+ this.childrenExtractor = childrenExtractor;
+ }
+
+ public RequestActorContext build(SagaRequest[] requests, Map<String, SagaTask> tasks, ActorRef sagaActor) {
+ RequestActorContext context = new RequestActorContext(childrenExtractor);
+
+ ActorRef rootActor = rootActor(context, tasks);
+ ActorRef leafActor = leafActor(context, tasks);
+
+ createRequestActors(requests, tasks, context);
+
+ linkActorsById(rootActor, requests, context);
+ addLeafToChildless(leafActor, requests, context);
+
+ context.addParent(NoOpSagaRequest.SAGA_START_REQUEST.id(), sagaActor);
+ context.addChild(NoOpSagaRequest.SAGA_END_REQUEST.id(), sagaActor);
+ return context;
+ }
+
+ private void linkActorsById(ActorRef rootActor, SagaRequest[] requests, RequestActorContext context) {
+ for (SagaRequest request : requests) {
+ if (isOrphan(request)) {
+ context.addParent(request.id(), rootActor);
+ context.addChild(NoOpSagaRequest.SAGA_START_REQUEST.id(), context.actorOf(request.id()));
+ } else {
+ for (String parent : request.parents()) {
+ context.addParent(request.id(), context.actorOf(parent));
+ context.addChild(parent, context.actorOf(request.id()));
+ }
+ }
+ }
+ }
+
+ private boolean isOrphan(SagaRequest request) {
+ return request.parents().length == 0;
+ }
+
+ private void createRequestActors(SagaRequest[] requests, Map<String, SagaTask> tasks, RequestActorContext context) {
+ for (SagaRequest request : requests) {
+ Props props = RequestActor.props(context, tasks.get(request.task()), request);
+ context.addActor(request.id(), actorSystem.actorOf(props));
+ }
+ }
+
+ private void addLeafToChildless(ActorRef leafActor, SagaRequest[] requests, RequestActorContext context) {
+ for (SagaRequest request : requests) {
+ if (context.childrenOf(request).isEmpty()) {
+ context.addParent(NoOpSagaRequest.SAGA_END_REQUEST.id(), context.actorOf(request.id()));
+ context.addChild(request.id(), leafActor);
+ }
+ }
+ }
+
+ private ActorRef rootActor(RequestActorContext context, Map<String, SagaTask> tasks) {
+ Props root = RequestActor.props(context, tasks.get(
+ NoOpSagaRequest.SAGA_START_REQUEST.task()), NoOpSagaRequest.SAGA_START_REQUEST);
+ ActorRef actor = actorSystem.actorOf(root);
+ context.addActor(NoOpSagaRequest.SAGA_START_REQUEST.id(), actor);
+ return actor;
+ }
+
+ private ActorRef leafActor(RequestActorContext context, Map<String, SagaTask> tasks) {
+ Props leaf = RequestActor.props(context, tasks.get(
+ NoOpSagaRequest.SAGA_END_REQUEST.task()), NoOpSagaRequest.SAGA_END_REQUEST);
+ ActorRef actor = actorSystem.actorOf(leaf);
+ context.addActor(NoOpSagaRequest.SAGA_END_REQUEST.id(), actor);
+ return actor;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorContext.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorContext.java
new file mode 100644
index 0000000..2e3149d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/RequestActorContext.java
@@ -0,0 +1,82 @@
+/*
+ * 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.saga.core.actors;
+
+import static java.util.Collections.emptyList;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Consumer;
+
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+
+class RequestActorContext {
+ private final Map<String, ActorRef> actors;
+ private final Map<String, List<ActorRef>> parents;
+ private final Map<String, List<ActorRef>> children;
+ private final FromJsonFormat<Set<String>> childrenExtractor;
+
+ RequestActorContext(
+ FromJsonFormat<Set<String>> childrenExtractor) {
+ this.actors = new HashMap<>();
+ this.children = new HashMap<>();
+ this.parents = new HashMap<>();
+ this.childrenExtractor = childrenExtractor;
+ }
+
+ void addActor(String id, ActorRef actorRef) {
+ actors.put(id, actorRef);
+ }
+
+ void addChild(String requestId, ActorRef ref) {
+ children.computeIfAbsent(requestId, k -> new ArrayList<>()).add(ref);
+ }
+
+ void addParent(String requestId, ActorRef ref) {
+ parents.computeIfAbsent(requestId, k -> new ArrayList<>()).add(ref);
+ }
+
+ ActorRef actorOf(String id) {
+ return actors.get(id);
+ }
+
+ Collection<ActorRef> parentsOf(SagaRequest request) {
+ return parents.getOrDefault(request.id(), emptyList());
+ }
+
+ Collection<ActorRef> childrenOf(SagaRequest request) {
+ return children.getOrDefault(request.id(), emptyList());
+ }
+
+ void forAll(Consumer<ActorRef> consumer) {
+ actors.values()
+ .stream()
+ .forEach(consumer);
+ }
+
+ Set<String> chosenChildren(SagaResponse response) {
+ return childrenExtractor.fromJson(response.body());
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortMessage.java
new file mode 100644
index 0000000..f23a079
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortMessage.java
@@ -0,0 +1,37 @@
+/*
+ * 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.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.FailedSagaResponse;
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class AbortMessage implements Message {
+ private final SagaResponse response;
+
+ public AbortMessage(Throwable e) {
+ response = new FailedSagaResponse(e);
+ }
+
+ public AbortMessage(SagaResponse response) {
+ this.response = response;
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortRecoveryMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortRecoveryMessage.java
new file mode 100644
index 0000000..951157d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/AbortRecoveryMessage.java
@@ -0,0 +1,33 @@
+/*
+ * 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.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class AbortRecoveryMessage implements Message {
+
+ private final SagaResponse response;
+
+ public AbortRecoveryMessage(SagaResponse response) {
+ this.response = response;
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensateMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensateMessage.java
new file mode 100644
index 0000000..5fd0701
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensateMessage.java
@@ -0,0 +1,32 @@
+/*
+ * 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.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class CompensateMessage implements Message {
+ private final SagaResponse response;
+
+ public CompensateMessage(SagaResponse response) {
+ this.response = response;
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensationRecoveryMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensationRecoveryMessage.java
new file mode 100644
index 0000000..421a092
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/CompensationRecoveryMessage.java
@@ -0,0 +1,22 @@
+/*
+ * 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.saga.core.actors.messages;
+
+public class CompensationRecoveryMessage implements Message {
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/FailMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/FailMessage.java
new file mode 100644
index 0000000..7a80ecb
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/FailMessage.java
@@ -0,0 +1,33 @@
+/*
+ * 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.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.FailedSagaResponse;
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class FailMessage implements Message {
+ private final SagaResponse response;
+
+ public FailMessage(Throwable e) {
+ response = new FailedSagaResponse(e);
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/Message.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/Message.java
new file mode 100644
index 0000000..3ed8187
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/Message.java
@@ -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.
+ */
+
+package org.apache.servicecomb.saga.core.actors.messages;
+
+public interface Message {
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactMessage.java
new file mode 100644
index 0000000..e4c57a2
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactMessage.java
@@ -0,0 +1,39 @@
+/*
+ * 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.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class TransactMessage implements Message {
+ private final SagaRequest request;
+ private final SagaResponse response;
+
+ public TransactMessage(SagaRequest request, SagaResponse response) {
+ this.request = request;
+ this.response = response;
+ }
+
+ public SagaRequest request() {
+ return request;
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactionRecoveryMessage.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactionRecoveryMessage.java
new file mode 100644
index 0000000..26ba05b
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/actors/messages/TransactionRecoveryMessage.java
@@ -0,0 +1,32 @@
+/*
+ * 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.saga.core.actors.messages;
+
+import org.apache.servicecomb.saga.core.SagaResponse;
+
+public class TransactionRecoveryMessage implements Message {
+ private final SagaResponse response;
+
+ public TransactionRecoveryMessage(SagaResponse response) {
+ this.response = response;
+ }
+
+ public SagaResponse response() {
+ return response;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaExecutionComponent.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaExecutionComponent.java
new file mode 100644
index 0000000..7a600d7
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaExecutionComponent.java
@@ -0,0 +1,87 @@
+/*
+ * 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.saga.core.application;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.UUID;
+
+import org.apache.servicecomb.saga.core.EventEnvelope;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.ToJsonFormat;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+import org.apache.servicecomb.saga.core.SagaEvent;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import org.apache.servicecomb.saga.infrastructure.EmbeddedEventStore;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class SagaExecutionComponent {
+
+ private final PersistentStore persistentStore;
+ private final FromJsonFormat<SagaDefinition> fromJsonFormat;
+ private final ToJsonFormat toJsonFormat;
+ private final SagaFactory sagaFactory;
+
+ public SagaExecutionComponent(
+ PersistentStore persistentStore,
+ FromJsonFormat<SagaDefinition> fromJsonFormat,
+ ToJsonFormat toJsonFormat,
+ SagaFactory sagaFactory) {
+ this.persistentStore = persistentStore;
+ this.fromJsonFormat = fromJsonFormat;
+ this.toJsonFormat = toJsonFormat;
+ this.sagaFactory = sagaFactory;
+ }
+
+ @Segment(name = "runSagaExecutionComponent", category = "application", library = "kamon")
+ public SagaResponse run(String requestJson) {
+ String sagaId = UUID.randomUUID().toString();
+ EventStore sagaLog = new EmbeddedEventStore();
+ SagaDefinition definition = fromJsonFormat.fromJson(requestJson);
+ Saga saga = sagaFactory.createSaga(requestJson, sagaId, sagaLog, definition);
+ return saga.run();
+ }
+
+ public void reanimate() {
+ Map<String, List<EventEnvelope>> pendingSagaEvents = persistentStore.findPendingSagaEvents();
+
+ for (Entry<String, List<EventEnvelope>> entry : pendingSagaEvents.entrySet()) {
+ EventStore eventStore = new EmbeddedEventStore();
+ eventStore.populate(entry.getValue());
+ SagaEvent event = entry.getValue().iterator().next().event;
+
+ String requestJson = event.json(toJsonFormat);
+ SagaDefinition definition = fromJsonFormat.fromJson(requestJson);
+
+ Saga saga = sagaFactory.createSaga(requestJson, event.sagaId, eventStore, definition);
+
+ saga.play();
+ saga.run();
+ }
+ }
+
+ public void terminate() throws Exception {
+ sagaFactory.terminate();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaFactory.java
new file mode 100644
index 0000000..b60b5e7
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/SagaFactory.java
@@ -0,0 +1,30 @@
+/*
+ * 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.saga.core.application;
+
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+
+public interface SagaFactory {
+ Saga createSaga(String requestJson, String sagaId, EventStore sagaLog, SagaDefinition definition);
+
+ boolean isTerminated();
+
+ void terminate() throws Exception;
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/FromJsonFormat.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/FromJsonFormat.java
new file mode 100644
index 0000000..ccfb408
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/FromJsonFormat.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.saga.core.application.interpreter;
+
+public interface FromJsonFormat<T> {
+
+ T fromJson(String requestJson);
+
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/RestRequestChecker.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/RestRequestChecker.java
new file mode 100644
index 0000000..7a65e48
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/application/interpreter/RestRequestChecker.java
@@ -0,0 +1,55 @@
+/*
+ * 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.saga.core.application.interpreter;
+
+import static java.util.Arrays.asList;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class RestRequestChecker {
+
+ private static final Set<String> validMethods = new HashSet<>(asList(
+ "GET",
+ "POST",
+ "PUT",
+ "DELETE"
+ ));
+
+ private RestRequestChecker() {
+ }
+
+ public static void checkParameters(String method, Map<String, Map<String, String>> params) {
+ if (method == null || !validMethods.contains(method.toUpperCase())) {
+ throw new IllegalArgumentException("Unsupported method " + method);
+ }
+
+ if (isDeleteOrGet(method) && hasBody(params)) {
+ throw new IllegalArgumentException("GET & DELETE request cannot enclose a body");
+ }
+ }
+
+ private static boolean isDeleteOrGet(String method) {
+ return "GET".equalsIgnoreCase(method) || "DELETE".equalsIgnoreCase(method);
+ }
+
+ private static boolean hasBody(Map<String, Map<String, String>> params) {
+ return params != null && (params.containsKey("form") || params.containsKey("json"));
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/ByLevelTraveller.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/ByLevelTraveller.java
new file mode 100644
index 0000000..22bc874
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/ByLevelTraveller.java
@@ -0,0 +1,83 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class ByLevelTraveller<T> implements Traveller<T> {
+
+ private final Collection<Node<T>> nodes;
+ private final Collection<Node<T>> nodesBuffer;
+
+ private final Queue<Node<T>> nodesWithoutParent = new LinkedList<>();
+ private final Map<Long, Set<Node<T>>> nodeParents = new HashMap<>();
+ private final TraversalDirection<T> traversalDirection;
+
+
+ public ByLevelTraveller(SingleLeafDirectedAcyclicGraph<T> dag, TraversalDirection<T> traversalDirection) {
+ this.nodes = new LinkedHashSet<>();
+ this.nodesBuffer = new LinkedList<>();
+ this.traversalDirection = traversalDirection;
+
+ nodesWithoutParent.offer(traversalDirection.root(dag));
+ }
+
+ @Segment(name = "travelNext", category = "application", library = "kamon")
+ @Override
+ public void next() {
+ nodes.addAll(nodesBuffer);
+ nodesBuffer.clear();
+ boolean buffered = false;
+
+ while (!nodesWithoutParent.isEmpty() && !buffered) {
+ Node<T> node = nodesWithoutParent.poll();
+ nodes.add(node);
+
+ for (Node<T> child : traversalDirection.children(node)) {
+ nodeParents.computeIfAbsent(child.id(), id -> new HashSet<>(traversalDirection.parents(child)));
+ nodeParents.get(child.id()).remove(node);
+
+ if (nodeParents.get(child.id()).isEmpty()) {
+ nodesWithoutParent.offer(child);
+ nodesBuffer.add(child);
+ buffered = true;
+ }
+ }
+ }
+ }
+
+ @Override
+ public boolean hasNext() {
+ return !nodesWithoutParent.isEmpty();
+ }
+
+ @Override
+ public Collection<Node<T>> nodes() {
+ return nodes;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromLeafTraversalDirection.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromLeafTraversalDirection.java
new file mode 100644
index 0000000..d97c40f
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromLeafTraversalDirection.java
@@ -0,0 +1,38 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.Set;
+
+public class FromLeafTraversalDirection<T> implements TraversalDirection<T> {
+
+ @Override
+ public Node<T> root(SingleLeafDirectedAcyclicGraph<T> dag) {
+ return dag.leaf();
+ }
+
+ @Override
+ public Set<Node<T>> parents(Node<T> node) {
+ return node.children();
+ }
+
+ @Override
+ public Set<Node<T>> children(Node<T> node) {
+ return node.parents();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromRootTraversalDirection.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromRootTraversalDirection.java
new file mode 100644
index 0000000..8c664ac
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/FromRootTraversalDirection.java
@@ -0,0 +1,38 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.Set;
+
+public class FromRootTraversalDirection<T> implements TraversalDirection<T> {
+
+ @Override
+ public Node<T> root(SingleLeafDirectedAcyclicGraph<T> dag) {
+ return dag.root();
+ }
+
+ @Override
+ public Set<Node<T>> parents(Node<T> node) {
+ return node.parents();
+ }
+
+ @Override
+ public Set<Node<T>> children(Node<T> node) {
+ return node.children();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaFactory.java
new file mode 100644
index 0000000..2048383
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaFactory.java
@@ -0,0 +1,80 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.GraphBasedSaga;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaContext;
+import org.apache.servicecomb.saga.core.SagaContextImpl;
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.apache.servicecomb.saga.infrastructure.ContextAwareEventStore;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+import org.apache.servicecomb.saga.core.SagaTaskFactory;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+
+public class GraphBasedSagaFactory implements SagaFactory {
+ private final AtomicBoolean isRunning = new AtomicBoolean(true);
+ private final FromJsonFormat<Set<String>> childrenExtractor;
+ private final Executor executorService;
+ private final GraphBuilder graphBuilder;
+ private final SagaTaskFactory sagaTaskFactory;
+
+ public GraphBasedSagaFactory(int retryDelay,
+ PersistentStore persistentStore,
+ FromJsonFormat<Set<String>> childrenExtractor,
+ ExecutorService executorService) {
+
+ this.childrenExtractor = childrenExtractor;
+ this.executorService = executorService;
+ this.sagaTaskFactory = new SagaTaskFactory(retryDelay, persistentStore);
+ this.graphBuilder = new GraphBuilder(new GraphCycleDetectorImpl<>());
+ }
+
+ @Override
+ public Saga createSaga(String requestJson, String sagaId, EventStore sagaLog, SagaDefinition definition) {
+ SagaContext sagaContext = new SagaContextImpl(childrenExtractor);
+
+ return new GraphBasedSaga(
+ sagaLog,
+ executorService,
+ sagaTaskFactory.sagaTasks(sagaId,
+ requestJson,
+ definition.policy(),
+ new ContextAwareEventStore(sagaLog, sagaContext)
+ ),
+ sagaContext,
+ graphBuilder.build(definition.requests()));
+ }
+
+ @Override
+ public boolean isTerminated() {
+ return !isRunning.get();
+ }
+
+ @Override
+ public void terminate() throws Exception {
+ isRunning.compareAndSet(true, false);
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBuilder.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBuilder.java
new file mode 100644
index 0000000..578cbb9
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphBuilder.java
@@ -0,0 +1,109 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaException;
+import org.apache.servicecomb.saga.core.SagaRequest;
+
+import kamon.annotation.EnableKamon;
+import kamon.annotation.Segment;
+
+@EnableKamon
+public class GraphBuilder {
+
+ private final GraphCycleDetector<SagaRequest> detector;
+
+ public GraphBuilder(GraphCycleDetector<SagaRequest> detector) {
+ this.detector = detector;
+ }
+
+ @Segment(name = "buildGraph", category = "application", library = "kamon")
+ public SingleLeafDirectedAcyclicGraph<SagaRequest> build(SagaRequest[] sagaRequests) {
+ Map<String, Node<SagaRequest>> requestNodes = requestsToNodes(sagaRequests);
+
+ SingleLeafDirectedAcyclicGraph<SagaRequest> graph = linkNodesToGraph(sagaRequests, requestNodes);
+ detectCycle(graph);
+ return graph;
+ }
+
+ private SingleLeafDirectedAcyclicGraph<SagaRequest> linkNodesToGraph(
+ SagaRequest[] sagaRequests,
+ Map<String, Node<SagaRequest>> requestNodes) {
+
+ Node<SagaRequest> root = rootNode(0);
+ Node<SagaRequest> leaf = leafNode(sagaRequests.length + 1);
+
+ for (SagaRequest sagaRequest : sagaRequests) {
+ if (isOrphan(sagaRequest)) {
+ root.addChild(requestNodes.get(sagaRequest.id()));
+ } else {
+ for (String parent : sagaRequest.parents()) {
+ requestNodes.get(parent).addChild(requestNodes.get(sagaRequest.id()));
+ }
+ }
+ }
+
+ requestNodes.values().stream()
+ .filter((node) -> node.children().isEmpty())
+ .forEach(node -> node.addChild(leaf));
+
+ return new SingleLeafDirectedAcyclicGraph<>(root, leaf);
+ }
+
+ private Node<SagaRequest> rootNode(int id) {
+ return new Node<>(
+ id,
+ NoOpSagaRequest.SAGA_START_REQUEST);
+ }
+
+ private Node<SagaRequest> leafNode(int id) {
+ return new Node<>(
+ id,
+ NoOpSagaRequest.SAGA_END_REQUEST);
+ }
+
+ private boolean isOrphan(SagaRequest sagaRequest) {
+ return sagaRequest.parents().length == 0;
+ }
+
+ private Map<String, Node<SagaRequest>> requestsToNodes(SagaRequest[] sagaRequests) {
+ long index = 1;
+ Map<String, Node<SagaRequest>> requestMap = new HashMap<>();
+ for (SagaRequest sagaRequest : sagaRequests) {
+ if (requestMap.containsKey(sagaRequest.id())) {
+ // TODO: 8/20/2017 add random id if user didn't provide one
+ throw new SagaException("Failed to interpret requests with duplicate request id: " + sagaRequest.id());
+ }
+ requestMap.put(sagaRequest.id(), new Node<>(index++, sagaRequest));
+ }
+ return requestMap;
+ }
+
+ private void detectCycle(SingleLeafDirectedAcyclicGraph<SagaRequest> graph) {
+ Set<Node<SagaRequest>> jointNodes = detector.cycleJoints(graph);
+
+ if (!jointNodes.isEmpty()) {
+ throw new SagaException("Cycle detected in the request graph at nodes " + jointNodes);
+ }
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetector.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetector.java
new file mode 100644
index 0000000..9b12bc8
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetector.java
@@ -0,0 +1,25 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.Set;
+
+public interface GraphCycleDetector<T> {
+
+ Set<Node<T>> cycleJoints(SingleLeafDirectedAcyclicGraph<T> graph);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorImpl.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorImpl.java
new file mode 100644
index 0000000..6011a98
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorImpl.java
@@ -0,0 +1,70 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Queue;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Cycle detection is based on topological sort with Kahn's algorithm.
+ *
+ * @see <a href="https://en.wikipedia.org/wiki/Topological_sorting">Topological Sorting</a>
+ */
+public class GraphCycleDetectorImpl<T> implements GraphCycleDetector<T> {
+
+ @Override
+ public Set<Node<T>> cycleJoints(SingleLeafDirectedAcyclicGraph<T> graph) {
+ Queue<Node<T>> orphanNodes = new LinkedList<>();
+ Map<Node<T>, Set<Node<T>>> nodeParents = new HashMap<>();
+
+ orphanNodes.add(graph.root());
+
+ traverse(orphanNodes, nodeParents);
+
+ return unvisitedNodes(nodeParents);
+ }
+
+ private void traverse(Queue<Node<T>> orphanNodes, Map<Node<T>, Set<Node<T>>> nodeParents) {
+ while (!orphanNodes.isEmpty()) {
+ Node<T> node = orphanNodes.poll();
+
+ node.children().forEach(child -> {
+ nodeParents.computeIfAbsent(child, n -> new HashSet<>(child.parents()))
+ .remove(node);
+
+ if (nodeParents.get(child).isEmpty()) {
+ orphanNodes.add(child);
+ }
+ });
+ }
+ }
+
+ private Set<Node<T>> unvisitedNodes(Map<Node<T>, Set<Node<T>>> nodeParents) {
+ return nodeParents.entrySet()
+ .parallelStream()
+ .filter(parents -> !parents.getValue().isEmpty())
+ .map(Entry::getKey)
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Node.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Node.java
new file mode 100644
index 0000000..0b3be0a
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Node.java
@@ -0,0 +1,86 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+public class Node<T> {
+ private final long id;
+ private final T value;
+ private final Set<Node<T>> children = new HashSet<>();
+ private final Set<Node<T>> parents = new HashSet<>();
+
+ public Node(long id, T value) {
+ this.id = id;
+ this.value = value;
+ }
+
+ long id() {
+ return id;
+ }
+
+ public T value() {
+ return value;
+ }
+
+ Set<Node<T>> parents() {
+ return parents;
+ }
+
+ public Set<Node<T>> children() {
+ return children;
+ }
+
+ public void addChild(Node<T> node) {
+ children.add(node);
+ node.parents.add(this);
+ }
+
+ public void addChildren(Collection<Node<T>> nodes) {
+ children.addAll(nodes);
+ nodes.forEach(node -> node.parents.add(this));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Node<?> node = (Node<?>) o;
+ return id == node.id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
+
+ @Override
+ public String toString() {
+ return "Node{" +
+ "id=" + id +
+ ", value=" + value +
+ '}';
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/SingleLeafDirectedAcyclicGraph.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/SingleLeafDirectedAcyclicGraph.java
new file mode 100644
index 0000000..cf0d19b
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/SingleLeafDirectedAcyclicGraph.java
@@ -0,0 +1,38 @@
+/*
+ * 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.saga.core.dag;
+
+public class SingleLeafDirectedAcyclicGraph<T> {
+
+ private final Node<T> root;
+ private final Node<T> leaf;
+
+ public SingleLeafDirectedAcyclicGraph(Node<T> root, Node<T> leaf) {
+
+ this.root = root;
+ this.leaf = leaf;
+ }
+
+ public Node<T> root() {
+ return root;
+ }
+
+ Node<T> leaf() {
+ return leaf;
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Traveller.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Traveller.java
new file mode 100644
index 0000000..e44848b
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/Traveller.java
@@ -0,0 +1,29 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.Collection;
+
+public interface Traveller<T> {
+
+ void next();
+
+ boolean hasNext();
+
+ Collection<Node<T>> nodes();
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/TraversalDirection.java b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/TraversalDirection.java
new file mode 100644
index 0000000..22f7343
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/core/dag/TraversalDirection.java
@@ -0,0 +1,29 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.Set;
+
+public interface TraversalDirection<T> {
+
+ Node<T> root(SingleLeafDirectedAcyclicGraph<T> dag);
+
+ Set<Node<T>> parents(Node<T> node);
+
+ Set<Node<T>> children(Node<T> node);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStore.java b/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStore.java
new file mode 100644
index 0000000..40fa71d
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStore.java
@@ -0,0 +1,56 @@
+/*
+ * 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.saga.infrastructure;
+
+import java.util.Iterator;
+
+import org.apache.servicecomb.saga.core.EventEnvelope;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.SagaContext;
+import org.apache.servicecomb.saga.core.SagaEvent;
+
+public class ContextAwareEventStore implements EventStore {
+ private final EventStore eventStore;
+ private final SagaContext sagaContext;
+
+ public ContextAwareEventStore(EventStore eventStore, SagaContext sagaContext) {
+ this.eventStore = eventStore;
+ this.sagaContext = sagaContext;
+ }
+
+ @Override
+ public void offer(SagaEvent sagaEvent) {
+ sagaEvent.gatherTo(sagaContext);
+ eventStore.offer(sagaEvent);
+ }
+
+ @Override
+ public void populate(Iterable<EventEnvelope> events) {
+ eventStore.populate(events);
+ }
+
+ @Override
+ public long size() {
+ return eventStore.size();
+ }
+
+ @Override
+ public Iterator<SagaEvent> iterator() {
+ return eventStore.iterator();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/EmbeddedEventStore.java b/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/EmbeddedEventStore.java
new file mode 100644
index 0000000..d342264
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/infrastructure/EmbeddedEventStore.java
@@ -0,0 +1,58 @@
+/*
+ * 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.saga.infrastructure;
+
+import java.lang.invoke.MethodHandles;
+import java.util.Iterator;
+import java.util.Queue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+import org.apache.servicecomb.saga.core.EventEnvelope;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.SagaEvent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class EmbeddedEventStore implements EventStore {
+ private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
+ private final Queue<SagaEvent> events = new LinkedBlockingQueue<>();
+
+ @Override
+ public void offer(SagaEvent sagaEvent) {
+ events.offer(sagaEvent);
+ log.info("Added event {}", sagaEvent);
+ }
+
+ @Override
+ public void populate(Iterable<EventEnvelope> events) {
+ for (EventEnvelope event : events) {
+ this.events.offer(event.event);
+ log.info("Populated event {}", event);
+ }
+ }
+
+ @Override
+ public long size() {
+ return events.size();
+ }
+
+ @Override
+ public Iterator<SagaEvent> iterator() {
+ return events.iterator();
+ }
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/transports/RestTransport.java b/saga-core/src/main/java/org/apache/servicecomb/saga/transports/RestTransport.java
new file mode 100644
index 0000000..3d5bda3
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/transports/RestTransport.java
@@ -0,0 +1,27 @@
+/*
+ * 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.saga.transports;
+
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.Transport;
+import java.util.Map;
+
+public interface RestTransport extends Transport {
+
+ SagaResponse with(String address, String path, String method, Map<String, Map<String, String>> params);
+}
diff --git a/saga-core/src/main/java/org/apache/servicecomb/saga/transports/TransportFactory.java b/saga-core/src/main/java/org/apache/servicecomb/saga/transports/TransportFactory.java
new file mode 100644
index 0000000..b34b5bd
--- /dev/null
+++ b/saga-core/src/main/java/org/apache/servicecomb/saga/transports/TransportFactory.java
@@ -0,0 +1,23 @@
+/*
+ * 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.saga.transports;
+
+public interface TransportFactory {
+
+ RestTransport restTransport();
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/BackwardRecoveryTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/BackwardRecoveryTest.java
new file mode 100644
index 0000000..4f03992
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/BackwardRecoveryTest.java
@@ -0,0 +1,57 @@
+/*
+ * 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.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class BackwardRecoveryTest {
+
+ private final String serviceName = "aaa";
+ private final Transaction transaction = mock(Transaction.class);
+ private final SagaTask sagaTask = mock(SagaTask.class);
+ private final SagaRequest sagaRequest = mock(SagaRequest.class);
+ private final SagaResponse parentResponse = mock(SagaResponse.class);
+ private final BackwardRecovery backwardRecovery = new BackwardRecovery();
+ private final RuntimeException exception = new RuntimeException("oops");
+
+ @Before
+ public void setUp() throws Exception {
+ when(sagaRequest.serviceName()).thenReturn(serviceName);
+ when(sagaRequest.transaction()).thenReturn(transaction);
+ }
+
+ @Test
+ public void blowsUpWhenTaskIsNotCommitted() {
+ doThrow(exception).when(transaction).send(serviceName, parentResponse);
+
+ try {
+ backwardRecovery.apply(sagaTask, sagaRequest, parentResponse);
+ expectFailing(RuntimeException.class);
+ } catch (RuntimeException ignored) {
+ }
+
+ verify(sagaTask).abort(sagaRequest, exception);
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompensationImpl.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompensationImpl.java
new file mode 100644
index 0000000..c974bc4
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompensationImpl.java
@@ -0,0 +1,27 @@
+/*
+ * 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.saga.core;
+
+import java.util.Map;
+
+public class CompensationImpl extends RestOperation implements Compensation {
+
+ public CompensationImpl(String path, String method, Map<String, Map<String, String>> params) {
+ super(path, method, params);
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaLogTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaLogTest.java
new file mode 100644
index 0000000..c7dea51
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaLogTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+import org.junit.Test;
+
+public class CompositeSagaLogTest {
+
+ private final SagaRequest request = mock(SagaRequest.class);
+ private final DummyEvent sagaEvent = new DummyEvent(request);
+ private final SagaLog embedded = mock(SagaLog.class);
+ private final SagaLog persistent = mock(SagaLog.class);
+
+ private final SagaLog compositeSagaLog = new CompositeSagaLog(embedded, persistent);
+
+ @Test
+ public void addsLogsToEmbeddedOnlyAfterPersisted() {
+ doThrow(RuntimeException.class).when(persistent).offer(sagaEvent);
+
+ try {
+ compositeSagaLog.offer(sagaEvent);
+ expectFailing(RuntimeException.class);
+ } catch (RuntimeException ignored) {
+ }
+
+ verify(embedded, never()).offer(sagaEvent);
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaResponseTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaResponseTest.java
new file mode 100644
index 0000000..be4b9f4
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/CompositeSagaResponseTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.saga.core;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class CompositeSagaResponseTest {
+
+ private final SagaResponse response1 = Mockito.mock(SagaResponse.class);
+ private final SagaResponse response2 = Mockito.mock(SagaResponse.class);
+
+ private final SagaResponse compositeSagaResponse = new CompositeSagaResponse(asList(response1, response2));
+
+ @Test
+ public void succeededOnlyWhenAllAreSuccessful() throws Exception {
+ when(response1.succeeded()).thenReturn(true);
+ when(response2.succeeded()).thenReturn(true);
+
+ assertThat(compositeSagaResponse.succeeded(), is(true));
+ }
+
+ @Test
+ public void failedWhenAnyIsNotSuccessful() throws Exception {
+ when(response1.succeeded()).thenReturn(true);
+ when(response2.succeeded()).thenReturn(false);
+
+ assertThat(compositeSagaResponse.succeeded(), is(false));
+ }
+
+ @Test
+ public void bodyCombinesAllResponseBodies() throws Exception {
+ when(response1.body()).thenReturn("{\n"
+ + " \"status\": 500,\n"
+ + " \"body\" : \"oops\"\n"
+ + "}\n");
+
+ when(response2.body()).thenReturn("{\n"
+ + " \"status\": 200,\n"
+ + " \"body\" : \"blah\"\n"
+ + "}\n");
+
+ assertThat(compositeSagaResponse.body(), is("[{\n"
+ + " \"status\": 500,\n"
+ + " \"body\" : \"oops\"\n"
+ + "}\n"
+ + ", {\n"
+ + " \"status\": 200,\n"
+ + " \"body\" : \"blah\"\n"
+ + "}\n"
+ + "]"));
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/DummyEvent.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/DummyEvent.java
new file mode 100644
index 0000000..c775afe
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/DummyEvent.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.saga.core;
+
+public class DummyEvent extends SagaEvent {
+
+ DummyEvent(SagaRequest payload) {
+ super("0", payload);
+ }
+
+ @Override
+ public void gatherTo(EventContext sagaContext) {
+
+ }
+
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/FallbackPolicyTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/FallbackPolicyTest.java
new file mode 100644
index 0000000..a2d1f32
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/FallbackPolicyTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.Randomness.uniquify;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+public class FallbackPolicyTest {
+
+ private final int numberOfRetries = 3;
+ private final String address = uniquify("address");
+
+ private final SagaResponse success = Mockito.mock(SagaResponse.class);
+ private final SagaResponse failure = Mockito.mock(SagaResponse.class);
+ private final Fallback fallback = Mockito.mock(Fallback.class);
+ private final Compensation compensation = Mockito.mock(Compensation.class);
+ private final FallbackPolicy fallbackPolicy = new FallbackPolicy(100);
+
+ @SuppressWarnings("ThrowableInstanceNeverThrown")
+ private final RuntimeException exception = new RuntimeException("oops");
+
+ @Before
+ public void setUp() throws Exception {
+ when(compensation.retries()).thenReturn(numberOfRetries);
+ when(fallback.send(address)).thenReturn(failure);
+ }
+
+ @Test
+ public void retriesTransportForSpecifiedTimes() {
+ when(compensation.send(address))
+ .thenThrow(exception)
+ .thenThrow(exception)
+ .thenReturn(success);
+
+ SagaResponse response = fallbackPolicy.apply(address, compensation, fallback);
+
+ assertThat(response, is(success));
+ verify(compensation, times(3)).send(address);
+ }
+
+ @Test
+ public void fallbackIfTransportFailedWithRetry() {
+ when(compensation.send(address)).thenThrow(exception);
+
+ SagaResponse response = fallbackPolicy.apply(address, compensation, fallback);
+ assertThat(response, is(failure));
+
+ verify(compensation, times(numberOfRetries + 1)).send(address);
+ verify(fallback).send(address);
+ }
+
+ @Test
+ public void retryUntilSuccessIfNumberOfRetriesIsNegative() throws InterruptedException {
+ reset(compensation);
+ when(compensation.retries()).thenReturn(-1);
+ when(compensation.send(address))
+ .thenThrow(exception, exception, exception, exception, exception)
+ .thenReturn(success);
+
+ SagaResponse response = fallbackPolicy.apply(address, compensation, fallback);
+
+ assertThat(response, is(success));
+ verify(fallback, never()).send(anyString());
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/ForwardRecoveryTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/ForwardRecoveryTest.java
new file mode 100644
index 0000000..a6e1dca
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/ForwardRecoveryTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.saga.core;
+
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class ForwardRecoveryTest {
+
+ private final SagaTask sagaTask = mock(SagaTask.class);
+
+ private final Transaction transaction = mock(Transaction.class);
+ private final SagaRequest sagaRequest = mock(SagaRequest.class);
+ private final SagaResponse parentResponse = mock(SagaResponse.class);
+
+ private final ForwardRecovery forwardRecovery = new ForwardRecovery();
+
+ private final String serviceName = "aaa";
+
+ @Before
+ public void setUp() {
+ when(sagaRequest.serviceName()).thenReturn(serviceName);
+ when(sagaRequest.transaction()).thenReturn(transaction);
+ when(sagaRequest.failRetryDelayMilliseconds()).thenReturn(300);
+ }
+
+ @Test
+ public void blowsUpWhenTaskIsNotCommittedWithFailRetryDelaySeconds() throws Exception {
+ doThrow(Exception.class).when(transaction).send(serviceName, parentResponse);
+
+ Thread t = new Thread(() -> forwardRecovery.apply(sagaTask, sagaRequest, parentResponse));
+ t.start();
+ Thread.sleep(400);
+ t.interrupt();
+
+ verify(transaction, times(2)).send(serviceName, parentResponse);
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/LongIdGeneratorTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/LongIdGeneratorTest.java
new file mode 100644
index 0000000..775866b
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/LongIdGeneratorTest.java
@@ -0,0 +1,36 @@
+/*
+ * 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.saga.core;
+
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+
+public class LongIdGeneratorTest {
+
+
+ private final LongIdGenerator generator = new LongIdGenerator();
+
+ @Test
+ public void generatesLongInSequence() {
+ for (long index = 0; index < 10; index++) {
+ assertThat(generator.nextId(), is(index + 1));
+ }
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/RestOperationTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/RestOperationTest.java
new file mode 100644
index 0000000..7f1990d
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/RestOperationTest.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.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonMap;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Test;
+
+public class RestOperationTest {
+
+ @Test
+ public void blowsUpWhenGetMethodWithForm() {
+ try {
+ new RestOperation("blah", "GET", singletonMap("form", emptyMap()));
+ expectFailing(IllegalArgumentException.class);
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("GET & DELETE request cannot enclose a body"));
+ }
+ }
+
+ @Test
+ public void blowsUpWhenGetMethodWithJson() {
+ try {
+ new RestOperation("blah", "GET", singletonMap("json", emptyMap()));
+ expectFailing(IllegalArgumentException.class);
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("GET & DELETE request cannot enclose a body"));
+ }
+ }
+
+ @Test
+ public void blowsUpWhenDeleteMethodWithForm() {
+ try {
+ new RestOperation("blah", "DELETE", singletonMap("form", emptyMap()));
+ expectFailing(IllegalArgumentException.class);
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("GET & DELETE request cannot enclose a body"));
+ }
+ }
+
+ @Test
+ public void blowsUpWhenDeleteMethodWithJson() {
+ try {
+ new RestOperation("blah", "DELETE", singletonMap("json", emptyMap()));
+ expectFailing(IllegalArgumentException.class);
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("GET & DELETE request cannot enclose a body"));
+ }
+ }
+
+ @Test
+ public void blowsUpWhenMethodIsNotSupported() {
+ try {
+ new RestOperation("blah", "foo", emptyMap());
+ expectFailing(IllegalArgumentException.class);
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("Unsupported method foo"));
+ }
+ }
+
+ @Test
+ public void blowsUpWhenMethodIsNull() {
+ try {
+ new RestOperation("blah", null, emptyMap());
+ expectFailing(IllegalArgumentException.class);
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage(), is("Unsupported method null"));
+ }
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/RetrySagaLogTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/RetrySagaLogTest.java
new file mode 100644
index 0000000..6da8f16
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/RetrySagaLogTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.saga.core;
+
+
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.Test;
+
+import org.apache.servicecomb.saga.core.SagaTaskFactory.RetrySagaLog;
+
+public class RetrySagaLogTest {
+
+ private final PersistentStore persistentStore = mock(PersistentStore.class);
+ private final SagaRequest sagaRequest = mock(SagaRequest.class);
+ private final SagaEvent dummyEvent = new DummyEvent(sagaRequest);
+ private final RetrySagaLog retrySagaLog = new RetrySagaLog(persistentStore, 100);
+
+ private volatile boolean interrupted = false;
+
+ @Test
+ public void retryUntilSuccessWhenEventIsNotPersisted() throws InterruptedException {
+ doThrow(RuntimeException.class).
+ doThrow(RuntimeException.class).
+ doThrow(RuntimeException.class).
+ doThrow(RuntimeException.class).
+ doThrow(RuntimeException.class).
+ doNothing().
+ when(persistentStore).offer(dummyEvent);
+
+ retrySagaLog.offer(dummyEvent);
+
+ verify(persistentStore, times(6)).offer(dummyEvent);
+ }
+
+ @Test
+ public void exitOnInterruption() throws InterruptedException {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ Future<?> future = executor.submit(() -> {
+ doThrow(RuntimeException.class).when(persistentStore).offer(dummyEvent);
+
+ retrySagaLog.offer(dummyEvent);
+ interrupted = true;
+ });
+
+ Thread.sleep(500);
+
+ assertThat(future.cancel(true), is(true));
+
+ await().atMost(2, TimeUnit.SECONDS).until(() -> interrupted);
+ executor.shutdown();
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEndTaskTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEndTaskTest.java
new file mode 100644
index 0000000..22f5fa1
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEndTaskTest.java
@@ -0,0 +1,47 @@
+/*
+ * 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.saga.core;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class SagaEndTaskTest {
+ private final SagaRequest request = mock(SagaRequest.class);
+ private final SagaLog sagaLog = mock(SagaLog.class);
+
+ private final String sagaId = "0";
+ private final SagaEndTask sagaEndTask = new SagaEndTask(sagaId, sagaLog);
+
+ @Test
+ public void emptyResponseOnSuccessfulEventPersistence() throws Exception {
+ ArgumentCaptor<SagaEndedEvent> argumentCaptor = ArgumentCaptor.forClass(SagaEndedEvent.class);
+ doNothing().when(sagaLog).offer(argumentCaptor.capture());
+
+ sagaEndTask.commit(request, SagaResponse.EMPTY_RESPONSE);
+
+ SagaEndedEvent event = argumentCaptor.getValue();
+ assertThat(event.sagaId, is(sagaId));
+ assertThat(event.json(null), is("{}"));
+ assertThat(event.payload(), is(request));
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEventMatcher.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEventMatcher.java
new file mode 100644
index 0000000..7e8289c
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaEventMatcher.java
@@ -0,0 +1,68 @@
+/*
+ * 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.saga.core;
+
+import java.util.Objects;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+
+public class SagaEventMatcher extends TypeSafeMatcher<SagaEvent> {
+
+ private final String sagaId;
+ private final Operation operation;
+ private final Class<?> aClass;
+
+ public static Matcher<SagaEvent> eventWith(String sagaId, Operation operation, Class<?> aClass) {
+ return new SagaEventMatcher(sagaId, operation, aClass);
+ }
+
+ static Matcher<SagaEvent> eventWith(Operation operation, Class<?> aClass) {
+ return eventWith("0", operation, aClass);
+ }
+
+ private SagaEventMatcher(String sagaId, Operation operation, Class<?> aClass) {
+ this.sagaId = sagaId;
+ this.operation = operation;
+ this.aClass = aClass;
+ }
+
+ @Override
+ protected void describeMismatchSafely(SagaEvent item, Description description) {
+ description
+ .appendText("SagaEvent {sagaId=" + item.sagaId + ", operation=" + operation(item) + ", class=" + item.getClass());
+ }
+
+ @Override
+ protected boolean matchesSafely(SagaEvent envelope) {
+ return Objects.equals(envelope.sagaId, sagaId)
+ && operation(envelope).equals(operation)
+ && envelope.getClass().equals(aClass);
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description
+ .appendText("SagaEvent {sagaId=" + sagaId + ", operation=" + operation + ", class=" + aClass.getCanonicalName());
+ }
+
+ private Operation operation(SagaEvent envelope) {
+ return operation instanceof Compensation ? envelope.payload().compensation() : envelope.payload().transaction();
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaExecutionComponentTestBase.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaExecutionComponentTestBase.java
new file mode 100644
index 0000000..b9e893f
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaExecutionComponentTestBase.java
@@ -0,0 +1,250 @@
+/*
+ * 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.saga.core;
+
+import static org.apache.servicecomb.saga.core.Operation.TYPE_REST;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.singletonList;
+import static java.util.Collections.singletonMap;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.awaitility.Awaitility.waitAtMost;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.TypeSafeMatcher;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.servicecomb.saga.core.application.SagaExecutionComponent;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import org.apache.servicecomb.saga.infrastructure.EmbeddedEventStore;
+
+@SuppressWarnings("unchecked")
+public abstract class SagaExecutionComponentTestBase {
+ private static final String requestJson = "[\n"
+ + " {\n"
+ + " \"id\": \"request-1\",\n"
+ + " \"serviceName\": \"aaa\",\n"
+ + " \"transaction\": {\n"
+ + " \"method\": \"post\",\n"
+ + " \"path\": \"/rest/as\"\n"
+ + " },\n"
+ + " \"compensation\": {\n"
+ + " \"method\": \"delete\",\n"
+ + " \"path\": \"/rest/as\"\n"
+ + " }\n"
+ + " }\n"
+ + "]\n";
+
+ private static final String sagaJson = "{\n"
+ + " \"policy\": \"ForwardRecovery\",\n"
+ + " \"requests\": " + requestJson + "\n"
+ + "}";
+
+ private static final String anotherRequestJson = "[\n"
+ + " {\n"
+ + " \"id\": \"request-2\",\n"
+ + " \"serviceName\": \"bbb\",\n"
+ + " \"transaction\": {\n"
+ + " \"method\": \"post\",\n"
+ + " \"path\": \"/rest/bs\"\n"
+ + " },\n"
+ + " \"compensation\": {\n"
+ + " \"method\": \"delete\",\n"
+ + " \"path\": \"/rest/bs\"\n"
+ + " }\n"
+ + " }\n"
+ + "]\n";
+
+ private static final String anotherSagaJson = "{\n"
+ + " \"policy\": \"ForwardRecovery\",\n"
+ + " \"requests\": " + anotherRequestJson + "\n"
+ + "}";
+
+ private final SagaRequest request1 = new SagaRequestImpl(
+ "request-1",
+ "aaa",
+ TYPE_REST,
+ new TransactionImpl("/rest/as", "post", emptyMap()),
+ new CompensationImpl("/rest/as", "delete", emptyMap())
+ );
+
+ private final SagaRequest request2 = new SagaRequestImpl(
+ "request-2",
+ "bbb",
+ TYPE_REST,
+ new TransactionImpl("/rest/bs", "post", emptyMap()),
+ new CompensationImpl("/rest/bs", "delete", emptyMap())
+ );
+
+ private final SagaDefinition definition1 = new SagaDefinition() {
+ @Override
+ public RecoveryPolicy policy() {
+ return new ForwardRecovery();
+ }
+
+ @Override
+ public SagaRequest[] requests() {
+ return new SagaRequest[]{request1};
+ }
+ };
+
+ private final SagaDefinition definition2 = new SagaDefinition() {
+ @Override
+ public RecoveryPolicy policy() {
+ return new BackwardRecovery();
+ }
+
+ @Override
+ public SagaRequest[] requests() {
+ return new SagaRequest[]{request2};
+ }
+ };
+
+ private final FromJsonFormat<SagaDefinition> fromJsonFormat = Mockito.mock(FromJsonFormat.class);
+ private final EmbeddedPersistentStore eventStore = new EmbeddedPersistentStore();
+
+ private final SagaExecutionComponent coordinator = new SagaExecutionComponent(
+ eventStore,
+ fromJsonFormat,
+ null,
+ sagaFactory(eventStore)
+ );
+
+ private final String sagaId = "1";
+
+ @Before
+ public void setUp() throws Exception {
+ when(fromJsonFormat.fromJson(sagaJson)).thenReturn(definition1);
+ when(fromJsonFormat.fromJson(anotherSagaJson)).thenReturn(definition2);
+ }
+
+ @Test
+ public void recoverSagaWithEventsFromEventStore() throws IOException {
+ eventStore.offer(new SagaStartedEvent(sagaId, sagaJson, NoOpSagaRequest.SAGA_START_REQUEST));
+ coordinator.reanimate();
+
+ assertThat(eventStore, contains(
+ eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+ eventWith(request1, TransactionStartedEvent.class),
+ eventWith(request1, TransactionEndedEvent.class),
+ eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class)
+ ));
+ }
+
+ @Test
+ public void runSagaWithEventStore() throws IOException {
+ SagaResponse response = coordinator.run(sagaJson);
+
+ assertThat(response, is(SagaResponse.EMPTY_RESPONSE));
+ assertThat(eventStore, contains(
+ eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+ eventWith(request1, TransactionStartedEvent.class),
+ eventWith(request1, TransactionEndedEvent.class),
+ eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class)
+ ));
+ }
+
+ @Test
+ public void processRequestsInParallel() {
+ CompletableFuture.runAsync(() -> coordinator.run(sagaJson));
+ CompletableFuture.runAsync(() -> coordinator.run(anotherSagaJson));
+
+ waitAtMost(2, SECONDS).until(() -> eventStore.size() == 8);
+
+ assertThat(eventStore, containsInAnyOrder(
+ eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+ eventWith(request1, TransactionStartedEvent.class),
+ eventWith(request1, TransactionEndedEvent.class),
+ eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class),
+ eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+ eventWith(request2, TransactionStartedEvent.class),
+ eventWith(request2, TransactionEndedEvent.class),
+ eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class)
+ ));
+ }
+
+ @Test
+ public void runSagaAfterRecovery() throws IOException {
+ eventStore.offer(new SagaStartedEvent(sagaId, sagaJson, NoOpSagaRequest.SAGA_START_REQUEST));
+ coordinator.reanimate();
+
+ coordinator.run(anotherSagaJson);
+
+ assertThat(eventStore, contains(
+ eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+ eventWith(request1, TransactionStartedEvent.class),
+ eventWith(request1, TransactionEndedEvent.class),
+ eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class),
+ eventWith(NoOpSagaRequest.SAGA_START_REQUEST, SagaStartedEvent.class),
+ eventWith(request2, TransactionStartedEvent.class),
+ eventWith(request2, TransactionEndedEvent.class),
+ eventWith(NoOpSagaRequest.SAGA_END_REQUEST, SagaEndedEvent.class)
+ ));
+ }
+
+ private Matcher<SagaEvent> eventWith(
+ SagaRequest sagaRequest,
+ Class<?> type) {
+
+ return new TypeSafeMatcher<SagaEvent>() {
+ @Override
+ protected boolean matchesSafely(SagaEvent event) {
+ SagaRequest request = event.payload();
+ return sagaRequest.equals(request)
+ && event.getClass().equals(type);
+ }
+
+ @Override
+ protected void describeMismatchSafely(SagaEvent item, Description mismatchDescription) {
+ SagaRequest request = item.payload();
+ mismatchDescription.appendText(
+ "SagaEvent {" + request + "}");
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(
+ "SagaEvent {" + sagaRequest + "}");
+ }
+ };
+ }
+
+ private class EmbeddedPersistentStore extends EmbeddedEventStore implements PersistentStore {
+
+ @Override
+ public Map<String, List<EventEnvelope>> findPendingSagaEvents() {
+ return singletonMap(sagaId, singletonList(
+ new EventEnvelope(1L, new SagaStartedEvent(sagaId, sagaJson, NoOpSagaRequest.SAGA_START_REQUEST))));
+ }
+ }
+
+ protected abstract SagaFactory sagaFactory(PersistentStore eventStore);
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaIntegrationTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaIntegrationTest.java
new file mode 100644
index 0000000..d005ee7
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaIntegrationTest.java
@@ -0,0 +1,687 @@
+/*
+ * 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.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.apache.servicecomb.saga.core.Compensation.SAGA_START_COMPENSATION;
+import static org.apache.servicecomb.saga.core.Operation.TYPE_REST;
+import static org.apache.servicecomb.saga.core.SagaEventMatcher.eventWith;
+import static org.apache.servicecomb.saga.core.SagaResponse.EMPTY_RESPONSE;
+import static org.apache.servicecomb.saga.core.SagaResponse.NONE_RESPONSE;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_END_TASK;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_REQUEST_TASK;
+import static org.apache.servicecomb.saga.core.SagaTask.SAGA_START_TASK;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_END_TRANSACTION;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_START_TRANSACTION;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+import static org.hamcrest.Matchers.anyOf;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.stubbing.Answer;
+
+import com.seanyinx.github.unit.scaffolding.Randomness;
+
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import org.apache.servicecomb.saga.core.dag.Node;
+import org.apache.servicecomb.saga.core.dag.SingleLeafDirectedAcyclicGraph;
+import org.apache.servicecomb.saga.infrastructure.ContextAwareEventStore;
+import org.apache.servicecomb.saga.infrastructure.EmbeddedEventStore;
+
+@SuppressWarnings("unchecked")
+public class SagaIntegrationTest {
+ private static final String sagaId = Randomness.uniquify("sagaId");
+
+ private final FromJsonFormat<Set<String>> childrenExtractor = mock(FromJsonFormat.class);
+ private final SagaContext sagaContext = new SagaContextImpl(childrenExtractor);
+ private final IdGenerator<Long> idGenerator = new LongIdGenerator();
+ private final EventStore eventStore = new EmbeddedEventStore();
+ private final ContextAwareEventStore sagaLog = new ContextAwareEventStore(eventStore, sagaContext);
+
+ private final Transaction transaction1 = mock(Transaction.class, "transaction1");
+ private final Transaction transaction2 = mock(Transaction.class, "transaction2");
+ private final Transaction transaction3 = mock(Transaction.class, "transaction3");
+ private final Transaction transaction4 = mock(Transaction.class, "transaction4");
+
+ private final Compensation compensation1 = mock(Compensation.class, "compensation1");
+ private final Compensation compensation2 = mock(Compensation.class, "compensation2");
+ private final Compensation compensation3 = mock(Compensation.class, "compensation3");
+ private final Compensation compensation4 = mock(Compensation.class, "compensation4");
+
+ private final Fallback fallback1 = mock(Fallback.class, "fallback1");
+
+ private final String requestJson = "{}";
+ private final SagaRequest request1 = request("request1", "service1", transaction1, compensation1, fallback1);
+ private final SagaRequest request2 = request("request2", "service2", transaction2, compensation2, request1.id());
+ private final SagaRequest request3 = request("request3", "service3", transaction3, compensation3, request1.id());
+ private final SagaRequest request4 = request("request4", "service4", transaction4, compensation4, request3.id());
+
+ private final SagaResponse transactionResponse1 = new SuccessfulSagaResponse("transaction1");
+ private final SagaResponse transactionResponse2 = new SuccessfulSagaResponse("transaction2");
+ private final SagaResponse transactionResponse3 = new SuccessfulSagaResponse("transaction3");
+ private final SagaResponse compensationResponse1 = new SuccessfulSagaResponse("compensation1");
+ private final SagaResponse compensationResponse2 = new SuccessfulSagaResponse("compensation2");
+ private final SagaResponse compensationResponse3 = new SuccessfulSagaResponse("compensation3");
+
+ @SuppressWarnings("ThrowableInstanceNeverThrown")
+ private final RuntimeException exception = new RuntimeException("oops");
+
+ private final Node<SagaRequest> node1 = new Node<>(1, request1);
+ private final Node<SagaRequest> node2 = new Node<>(2, request2);
+ private final Node<SagaRequest> node3 = new Node<>(3, request3);
+ private final Node<SagaRequest> node4 = new Node<>(4, request4);
+ private final Node<SagaRequest> root = new Node<>(0, NoOpSagaRequest.SAGA_START_REQUEST);
+ private final Node<SagaRequest> leaf = new Node<>(5, NoOpSagaRequest.SAGA_END_REQUEST);
+ private final SingleLeafDirectedAcyclicGraph<SagaRequest> sagaTaskGraph = new SingleLeafDirectedAcyclicGraph<>(root, leaf);
+
+ private Saga saga;
+ private final Map<String, SagaTask> tasks = new HashMap<>();
+
+ // root - node1 - node2 - leaf
+ @Before
+ public void setUp() throws Exception {
+ when(childrenExtractor.fromJson(anyString())).thenReturn(emptySet());
+ when(childrenExtractor.fromJson(NONE_RESPONSE.body())).thenReturn(setOf("none"));
+
+ when(transaction1.send(request1.serviceName(), EMPTY_RESPONSE)).thenReturn(transactionResponse1);
+ when(transaction2.send(request2.serviceName(), transactionResponse1)).thenReturn(transactionResponse2);
+ when(transaction3.send(request3.serviceName(), transactionResponse1)).thenReturn(transactionResponse3);
+
+ when(compensation1.send(request1.serviceName(), compensationResponse2)).thenReturn(compensationResponse1);
+ when(compensation2.send(request2.serviceName(), compensationResponse3)).thenReturn(compensationResponse2);
+ when(compensation3.send(request3.serviceName(), EMPTY_RESPONSE)).thenReturn(compensationResponse3);
+
+ root.addChild(node1);
+ node1.addChild(node2);
+ node2.addChild(leaf);
+
+ SagaStartTask sagaStartTask = new SagaStartTask(sagaId, requestJson, sagaLog);
+ SagaEndTask sagaEndTask = new SagaEndTask(sagaId, sagaLog);
+ RequestProcessTask processTask = requestProcessTask(new BackwardRecovery());
+
+ tasks.put(SAGA_START_TASK, sagaStartTask);
+ tasks.put(SAGA_REQUEST_TASK, processTask);
+ tasks.put(SAGA_END_TASK, sagaEndTask);
+
+ saga = new GraphBasedSaga(eventStore, tasks, sagaContext, sagaTaskGraph);
+ }
+
+ @Test
+ public void transactionsAreRunSuccessfully() {
+ saga.run();
+
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+ verify(transaction2).send(request2.serviceName(), transactionResponse1);
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ }
+
+ // root - node1 - node2 - leaf
+ // \_ node3 _/
+ @Test
+ public void compensateCommittedTransactionsOnFailure() {
+ addExtraChildToNode1();
+
+ // barrier to make sure the two transactions starts at the same time
+ CyclicBarrier barrier = new CyclicBarrier(2);
+ when(transaction2.send(request2.serviceName(), transactionResponse1))
+ .thenAnswer(
+ withAnswer(() -> {
+ barrier.await();
+ Thread.sleep(100);
+ throw exception;
+ }));
+
+ when(transaction3.send(request3.serviceName(), transactionResponse1))
+ .thenAnswer(
+ withAnswer(() -> {
+ barrier.await();
+ return transactionResponse3;
+ }));
+
+ saga.run();
+
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ anyOf(eventWith(sagaId, transaction2, TransactionStartedEvent.class), eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+ anyOf(eventWith(sagaId, transaction2, TransactionStartedEvent.class), eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+ eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionAbortedEvent.class),
+ eventWith(sagaId, compensation3, TransactionCompensatedEvent.class),
+ eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)));
+
+ verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+ verify(transaction2).send(request2.serviceName(), transactionResponse1);
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3).send(request3.serviceName());
+ }
+
+ @Test
+ public void skipIgnoredTransaction() throws Exception {
+ addExtraChildToNode1();
+
+ when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf(request3.id()));
+
+ saga.run();
+
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void skipAllIgnoredTransactions() throws Exception {
+ node1.addChild(node3);
+ node3.addChild(node4);
+ node4.addChild(leaf);
+
+ when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf("none"));
+
+ saga.run();
+
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction4, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ verify(compensation4, never()).send(request4.serviceName());
+ }
+
+ @Test
+ public void doNotCompensateIgnoredTransactions() throws Exception {
+ node1.addChild(node3);
+ node3.addChild(node4);
+ node4.addChild(leaf);
+
+ when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf(request3.id()));
+
+ when(transaction4.send(request4.serviceName(), transactionResponse3)).thenThrow(exception);
+
+ saga.run();
+
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction4, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction4, TransactionAbortedEvent.class),
+ eventWith(sagaId, transaction3, TransactionCompensatedEvent.class),
+ eventWith(sagaId, transaction1, TransactionCompensatedEvent.class),
+ eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+ verify(transaction4).send(request4.serviceName(), transactionResponse3);
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation3).send(request3.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation4, never()).send(request4.serviceName());
+ }
+
+ // root - node1 - node2 - leaf
+ // \_ node3 _/
+ @Test
+ public void redoHangingTransactionsOnFailure() throws InterruptedException {
+ addExtraChildToNode1();
+
+ // barrier to make sure the two transactions starts at the same time
+ CyclicBarrier barrier = new CyclicBarrier(2);
+ when(transaction3.send(request3.serviceName(), transactionResponse1))
+ .thenAnswer(withAnswer(() -> {
+ barrier.await();
+ throw exception;
+ }));
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ when(transaction2.send(request2.serviceName(), transactionResponse1))
+ .thenAnswer(withAnswer(() -> {
+ barrier.await();
+ latch.await();
+ return transactionResponse2;
+ })).thenReturn(transactionResponse2);
+
+ saga.run();
+
+ // the ordering of events may not be consistence due to concurrent processing of requests
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ anyOf(
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+ anyOf(
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class)),
+ eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+ eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)));
+
+ verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+ verify(transaction2, times(2)).send(request2.serviceName(), transactionResponse1);
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+
+ latch.countDown();
+ }
+
+ @Test
+ public void retriesFailedTransactionTillSuccess() {
+ RequestProcessTask processTask = requestProcessTask(new ForwardRecovery());
+ tasks.put(SAGA_REQUEST_TASK, processTask);
+
+ when(transaction2.send(request2.serviceName(), transactionResponse1))
+ .thenThrow(exception).thenThrow(exception).thenReturn(transactionResponse2);
+
+ saga.run();
+
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+ verify(transaction2, times(3)).send(request2.serviceName(), transactionResponse1);
+
+ verify(compensation1, never()).send(anyString(), any(SagaResponse.class));
+ verify(compensation2, never()).send(anyString(), any(SagaResponse.class));
+ }
+
+ @Test
+ public void fallbackWhenCompensationFailed() {
+ int retries = 3;
+
+ when(transaction2.send(request2.serviceName(), transactionResponse1)).thenThrow(exception);
+ when(compensation1.send(request1.serviceName())).thenThrow(exception);
+ when(compensation1.retries()).thenReturn(retries);
+
+ saga.run();
+
+ verify(transaction1).send(request1.serviceName(), EMPTY_RESPONSE);
+ verify(transaction2).send(request2.serviceName(), transactionResponse1);
+
+ verify(compensation1, times(retries + 1)).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+
+ verify(fallback1).send(request1.serviceName());
+ }
+
+ @Test
+ public void restoresSagaToTransactionStateByPlayingAllEvents() {
+ addExtraChildToNode1();
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1, transactionResponse1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2, transactionResponse2))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresPartialTransactionByPlayingAllEvents() {
+ addExtraChildToNode1();
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1, transactionResponse1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2, transactionResponse2)),
+ envelope(new TransactionStartedEvent(sagaId, request3))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresToCompensationFromAbortedTransactionByPlayingAllEvents() {
+ addExtraChildToNode1();
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2)),
+ envelope(new TransactionStartedEvent(sagaId, request3)),
+ envelope(new TransactionAbortedEvent(sagaId, request3, exception))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+ eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+ eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresSagaToCompensationStateByPlayingAllEvents() {
+ addExtraChildToNode1();
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2)),
+ envelope(new TransactionStartedEvent(sagaId, request3)),
+ envelope(new TransactionEndedEvent(sagaId, request3)),
+ envelope(new TransactionCompensatedEvent(sagaId, request2))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+ eventWith(sagaId, compensation3, TransactionCompensatedEvent.class),
+ eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresPartialCompensationByPlayingAllEvents() {
+ addExtraChildToNode1();
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2)),
+ envelope(new TransactionStartedEvent(sagaId, request3)),
+ envelope(new TransactionAbortedEvent(sagaId, request3, exception)),
+ envelope(new TransactionCompensatedEvent(sagaId, request2))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+ eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+ eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ eventWith(sagaId, SAGA_START_COMPENSATION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresSagaToEndStateByPlayingAllEvents() {
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, contains(
+ eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ }
+
+ @Test
+ public void failFastIfSagaLogIsDown() throws Exception {
+ SagaLog sagaLog = mock(SagaLog.class);
+ tasks.put(SAGA_START_TASK, new SagaStartTask(sagaId, requestJson, sagaLog));
+
+ doThrow(RuntimeException.class).when(sagaLog).offer(any(SagaStartedEvent.class));
+
+ try {
+ saga.run();
+ expectFailing(SagaStartFailedException.class);
+ } catch (SagaStartFailedException e) {
+ assertThat(e.getMessage(), is("Failed to persist SagaStartedEvent for " + requestJson));
+ }
+
+ verify(sagaLog).offer(any(SagaStartedEvent.class));
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ }
+
+ private Answer<SagaResponse> withAnswer(Callable<SagaResponse> callable) {
+ return invocationOnMock -> callable.call();
+ }
+
+ private EventEnvelope envelope(SagaEvent event) {
+ return new EventEnvelope(idGenerator.nextId(), event);
+ }
+
+ private void addExtraChildToNode1() {
+ node1.addChild(node3);
+ node3.addChild(leaf);
+ }
+
+ private SagaRequest request(String requestId,
+ String serviceName,
+ Transaction transaction,
+ Compensation compensation,
+ String... parentIds) {
+
+ return new SagaRequestImpl(requestId, serviceName, TYPE_REST, transaction, compensation, parentIds);
+ }
+
+ private SagaRequest request(String requestId,
+ String serviceName,
+ Transaction transaction,
+ Compensation compensation,
+ Fallback fallback) {
+
+ return new SagaRequestImpl(requestId, serviceName, TYPE_REST, transaction, compensation, fallback);
+ }
+
+ private HashSet<String> setOf(String requestId) {
+ return new HashSet<>(singletonList(requestId));
+ }
+
+ private RequestProcessTask requestProcessTask(RecoveryPolicy recoveryPolicy) {
+ return new RequestProcessTask(sagaId, sagaLog, recoveryPolicy, new FallbackPolicy(100));
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaStartTaskTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaStartTaskTest.java
new file mode 100644
index 0000000..b1d0c4e
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/SagaStartTaskTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.saga.core;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+public class SagaStartTaskTest {
+ private final SagaRequest request = mock(SagaRequest.class);
+ private final SagaLog sagaLog = mock(SagaLog.class);
+
+ private final String sagaId = "0";
+ private final String requestJson = null;
+ private final SagaStartTask sagaStartTask = new SagaStartTask(sagaId, requestJson, sagaLog);
+
+ @Test
+ public void emptyResponseOnSuccessfulEventPersistence() throws Exception {
+ ArgumentCaptor<SagaStartedEvent> argumentCaptor = ArgumentCaptor.forClass(SagaStartedEvent.class);
+ doNothing().when(sagaLog).offer(argumentCaptor.capture());
+
+ sagaStartTask.commit(request, SagaResponse.EMPTY_RESPONSE);
+
+ SagaStartedEvent event = argumentCaptor.getValue();
+ assertThat(event.sagaId, is(sagaId));
+ assertThat(event.json(null), is(requestJson));
+ assertThat(event.payload(), is(request));
+ }
+
+ @Test
+ public void blowsUpWhenEventIsNotPersisted() {
+ doThrow(RuntimeException.class).when(sagaLog).offer(any(SagaStartedEvent.class));
+
+ try {
+ sagaStartTask.commit(request, SagaResponse.EMPTY_RESPONSE);
+ expectFailing(SagaStartFailedException.class);
+ } catch (SagaStartFailedException e) {
+ assertThat(e.getMessage(), startsWith("Failed to persist SagaStartedEvent"));
+ }
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/TransactionImpl.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/TransactionImpl.java
new file mode 100644
index 0000000..a59b142
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/TransactionImpl.java
@@ -0,0 +1,27 @@
+/*
+ * 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.saga.core;
+
+import java.util.Map;
+
+public class TransactionImpl extends RestOperation implements Transaction {
+
+ public TransactionImpl(String path, String method, Map<String, Map<String, String>> params) {
+ super(path, method, params);
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaExecutionComponentTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaExecutionComponentTest.java
new file mode 100644
index 0000000..01f8adb
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaExecutionComponentTest.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.saga.core.actors;
+
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaExecutionComponentTestBase;
+
+
+public class ActorBasedSagaExecutionComponentTest extends SagaExecutionComponentTestBase {
+
+ @Override
+ protected SagaFactory sagaFactory(PersistentStore eventStore) {
+ return new ActorBasedSagaFactory(500, eventStore, null);
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaIntegrationTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaIntegrationTest.java
new file mode 100644
index 0000000..ac649f0
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/ActorBasedSagaIntegrationTest.java
@@ -0,0 +1,691 @@
+/*
+ * 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.saga.core.actors;
+
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_END_TRANSACTION;
+import static org.apache.servicecomb.saga.core.Transaction.SAGA_START_TRANSACTION;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singletonList;
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.HashSet;
+import java.util.Set;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.CyclicBarrier;
+
+import org.apache.servicecomb.saga.core.Compensation;
+import org.apache.servicecomb.saga.core.EventEnvelope;
+import org.apache.servicecomb.saga.core.EventStore;
+import org.apache.servicecomb.saga.core.Fallback;
+import org.apache.servicecomb.saga.core.ForwardRecovery;
+import org.apache.servicecomb.saga.core.IdGenerator;
+import org.apache.servicecomb.saga.core.LongIdGenerator;
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.Operation;
+import org.apache.servicecomb.saga.core.Saga;
+import org.apache.servicecomb.saga.core.SagaEventMatcher;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaRequestImpl;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.SagaStartedEvent;
+import org.apache.servicecomb.saga.core.SuccessfulSagaResponse;
+import org.apache.servicecomb.saga.core.TransactionCompensatedEvent;
+import org.apache.servicecomb.saga.core.TransactionStartedEvent;
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+import org.hamcrest.CoreMatchers;
+import org.hamcrest.collection.IsIterableContainingInOrder;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.mockito.stubbing.Answer;
+
+import com.seanyinx.github.unit.scaffolding.Randomness;
+
+import org.apache.servicecomb.saga.core.BackwardRecovery;
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaDefinition;
+import org.apache.servicecomb.saga.core.SagaEndedEvent;
+import org.apache.servicecomb.saga.core.SagaEvent;
+import org.apache.servicecomb.saga.core.Transaction;
+import org.apache.servicecomb.saga.core.TransactionAbortedEvent;
+import org.apache.servicecomb.saga.core.TransactionEndedEvent;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import org.apache.servicecomb.saga.infrastructure.EmbeddedEventStore;
+
+@SuppressWarnings("unchecked")
+public class ActorBasedSagaIntegrationTest {
+ private static final String sagaId = Randomness.uniquify("sagaId");
+
+ private final FromJsonFormat<Set<String>> childrenExtractor = mock(FromJsonFormat.class);
+ private final IdGenerator<Long> idGenerator = new LongIdGenerator();
+ private final EventStore eventStore = new EmbeddedEventStore();
+
+ private final PersistentStore persistentStore = mock(PersistentStore.class);
+ private final SagaDefinition sagaDefinition = mock(SagaDefinition.class);
+
+ private final Transaction transaction1 = mock(Transaction.class, "transaction1");
+ private final Transaction transaction2 = mock(Transaction.class, "transaction2");
+ private final Transaction transaction3 = mock(Transaction.class, "transaction3");
+ private final Transaction transaction4 = mock(Transaction.class, "transaction4");
+
+ private final Compensation compensation1 = mock(Compensation.class, "compensation1");
+ private final Compensation compensation2 = mock(Compensation.class, "compensation2");
+ private final Compensation compensation3 = mock(Compensation.class, "compensation3");
+ private final Compensation compensation4 = mock(Compensation.class, "compensation4");
+
+ private final Fallback fallback1 = mock(Fallback.class, "fallback1");
+
+ private final String requestJson = "{}";
+ private final SagaRequest request1 = request("request1", "service1", transaction1, compensation1, fallback1);
+ private final SagaRequest request2 = request("request2", "service2", transaction2, compensation2, request1.id());
+ private final SagaRequest request3 = request("request3", "service3", transaction3, compensation3, request1.id());
+ private final SagaRequest request4 = request("request4", "service4", transaction4, compensation4, request3.id());
+
+ private final SagaResponse transactionResponse1 = new SuccessfulSagaResponse("transaction1");
+ private final SagaResponse transactionResponse2 = new SuccessfulSagaResponse("transaction2");
+ private final SagaResponse transactionResponse3 = new SuccessfulSagaResponse("transaction3");
+ private final SagaResponse compensationResponse1 = new SuccessfulSagaResponse("compensation1");
+ private final SagaResponse compensationResponse2 = new SuccessfulSagaResponse("compensation2");
+ private final SagaResponse compensationResponse3 = new SuccessfulSagaResponse("compensation3");
+
+ @SuppressWarnings("ThrowableInstanceNeverThrown")
+ private final RuntimeException exception = new RuntimeException("oops");
+
+ private Saga saga;
+ private final SagaFactory sagaFactory = new ActorBasedSagaFactory(100, persistentStore, childrenExtractor);
+
+ // root - node1 - node2 - leaf
+ @Before
+ public void setUp() throws Exception {
+ when(sagaDefinition.policy()).thenReturn(new BackwardRecovery());
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2});
+
+ when(childrenExtractor.fromJson(anyString())).thenReturn(emptySet());
+ when(childrenExtractor.fromJson(SagaResponse.NONE_RESPONSE.body())).thenReturn(setOf("none"));
+
+ when(transaction1.send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE)).thenReturn(transactionResponse1);
+ when(transaction2.send(request2.serviceName(), transactionResponse1)).thenReturn(transactionResponse2);
+ when(transaction3.send(request3.serviceName(), transactionResponse1)).thenReturn(transactionResponse3);
+
+ when(compensation1.send(request1.serviceName(), compensationResponse2)).thenReturn(compensationResponse1);
+ when(compensation2.send(request2.serviceName(), compensationResponse3)).thenReturn(compensationResponse2);
+ when(compensation3.send(request3.serviceName(), SagaResponse.EMPTY_RESPONSE)).thenReturn(compensationResponse3);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ sagaFactory.terminate();
+ assertTrue(sagaFactory.isTerminated());
+ }
+
+ @Test
+ public void transactionsAreRunSuccessfully() {
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+ saga.run();
+
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+ verify(transaction2).send(request2.serviceName(), transactionResponse1);
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ }
+
+ // root - node1 - node2 - leaf
+ // \_ node3 _/
+ @Test
+ public void compensateCommittedTransactionsOnFailure() {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ // barrier to make sure the two transactions starts at the same time
+ CyclicBarrier barrier = new CyclicBarrier(2);
+ when(transaction2.send(request2.serviceName(), transactionResponse1))
+ .thenAnswer(
+ withAnswer(() -> {
+ barrier.await();
+ Thread.sleep(100);
+ throw exception;
+ }));
+
+ when(transaction3.send(request3.serviceName(), transactionResponse1))
+ .thenAnswer(
+ withAnswer(() -> {
+ barrier.await();
+ return transactionResponse3;
+ }));
+
+ saga.run();
+
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ CoreMatchers.anyOf(SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class), SagaEventMatcher
+ .eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+ CoreMatchers.anyOf(SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class), SagaEventMatcher
+ .eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionAbortedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation3, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)));
+
+ verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+ verify(transaction2).send(request2.serviceName(), transactionResponse1);
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3).send(request3.serviceName());
+ }
+
+ @Test
+ public void skipIgnoredTransaction() throws Exception {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf(request3.id()));
+
+ saga.run();
+
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void skipAllIgnoredTransactions() throws Exception {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3, request4});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf("none"));
+
+ saga.run();
+
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction4, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ verify(compensation4, never()).send(request4.serviceName());
+ }
+
+ @Test
+ public void doNotCompensateIgnoredTransactions() throws Exception {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3, request4});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ when(childrenExtractor.fromJson(transactionResponse1.body())).thenReturn(setOf(request3.id()));
+
+ when(transaction4.send(request4.serviceName(), transactionResponse3)).thenThrow(exception);
+
+ saga.run();
+
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction4, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction4, TransactionAbortedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+ verify(transaction4).send(request4.serviceName(), transactionResponse3);
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation3).send(request3.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation4, never()).send(request4.serviceName());
+ }
+
+ // TODO: 2017/10/31 actor will hang and its parent and children will be blocked without its response, timeout must be applied
+ @Ignore
+ // root - node1 - node2 - leaf
+ // \_ node3 _/
+ @Test
+ public void redoHangingTransactionsOnFailure() throws InterruptedException {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ // barrier to make sure the two transactions starts at the same time
+ CyclicBarrier barrier = new CyclicBarrier(2);
+ when(transaction3.send(request3.serviceName(), transactionResponse1))
+ .thenAnswer(withAnswer(() -> {
+ barrier.await();
+ throw exception;
+ }));
+
+ CountDownLatch latch = new CountDownLatch(1);
+
+ when(transaction2.send(request2.serviceName(), transactionResponse1))
+ .thenAnswer(withAnswer(() -> {
+ barrier.await();
+ latch.await(1, SECONDS);
+ return transactionResponse2;
+ })).thenReturn(transactionResponse2);
+
+ saga.run();
+
+ // the ordering of events may not be consistence due to concurrent processing of requests
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ CoreMatchers.anyOf(
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class)),
+ CoreMatchers.anyOf(
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class)),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)));
+
+ verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+ verify(transaction2, times(2)).send(request2.serviceName(), transactionResponse1);
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+
+ latch.countDown();
+ }
+
+ @Test
+ public void retriesFailedTransactionTillSuccess() {
+ when(sagaDefinition.policy()).thenReturn(new ForwardRecovery());
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ when(transaction2.send(request2.serviceName(), transactionResponse1))
+ .thenThrow(exception).thenThrow(exception).thenReturn(transactionResponse2);
+
+ saga.run();
+
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+ verify(transaction2, times(3)).send(request2.serviceName(), transactionResponse1);
+
+ verify(compensation1, never()).send(anyString(), any(SagaResponse.class));
+ verify(compensation2, never()).send(anyString(), any(SagaResponse.class));
+ }
+
+ @Test
+ public void fallbackWhenCompensationFailed() {
+ int retries = 3;
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ when(transaction2.send(request2.serviceName(), transactionResponse1)).thenThrow(exception);
+ when(compensation1.send(request1.serviceName())).thenThrow(exception);
+ when(compensation1.retries()).thenReturn(retries);
+
+ saga.run();
+
+ verify(transaction1).send(request1.serviceName(), SagaResponse.EMPTY_RESPONSE);
+ verify(transaction2).send(request2.serviceName(), transactionResponse1);
+
+ verify(compensation1, times(retries + 1)).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+
+ verify(fallback1).send(request1.serviceName());
+ }
+
+ @Test
+ public void restoresSagaToTransactionStateByPlayingAllEvents() {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1, transactionResponse1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2, transactionResponse2))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresPartialTransactionByPlayingAllEvents() {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1, transactionResponse1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2, transactionResponse2)),
+ envelope(new TransactionStartedEvent(sagaId, request3))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3).send(request3.serviceName(), transactionResponse1);
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresToCompensationFromAbortedTransactionByPlayingAllEvents() {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2)),
+ envelope(new TransactionStartedEvent(sagaId, request3)),
+ envelope(new TransactionAbortedEvent(sagaId, request3, exception))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresSagaToCompensationStateByPlayingAllEvents() {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2)),
+ envelope(new TransactionStartedEvent(sagaId, request3)),
+ envelope(new TransactionAbortedEvent(sagaId, request3, exception)),
+ envelope(new TransactionCompensatedEvent(sagaId, request2))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresPartialCompensationByPlayingAllEvents() {
+ when(sagaDefinition.requests()).thenReturn(new SagaRequest[]{request1, request2, request3});
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2)),
+ envelope(new TransactionStartedEvent(sagaId, request3)),
+ envelope(new TransactionAbortedEvent(sagaId, request3, exception)),
+ envelope(new TransactionCompensatedEvent(sagaId, request2))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction3, TransactionAbortedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation2, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, compensation1, TransactionCompensatedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, Compensation.SAGA_START_COMPENSATION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction3, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ verify(compensation3, never()).send(request3.serviceName());
+ }
+
+ @Test
+ public void restoresSagaToEndStateByPlayingAllEvents() {
+ saga = sagaFactory.createSaga(requestJson, sagaId, eventStore, sagaDefinition);
+ Iterable<EventEnvelope> events = asList(
+ envelope(new SagaStartedEvent(sagaId, requestJson, NoOpSagaRequest.SAGA_START_REQUEST)),
+ envelope(new TransactionStartedEvent(sagaId, request1)),
+ envelope(new TransactionEndedEvent(sagaId, request1)),
+ envelope(new TransactionStartedEvent(sagaId, request2)),
+ envelope(new TransactionEndedEvent(sagaId, request2))
+ );
+
+ eventStore.populate(events);
+ saga.play();
+
+ saga.run();
+ assertThat(eventStore, IsIterableContainingInOrder.contains(
+ SagaEventMatcher.eventWith(sagaId, SAGA_START_TRANSACTION, SagaStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction1, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionStartedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, transaction2, TransactionEndedEvent.class),
+ SagaEventMatcher.eventWith(sagaId, SAGA_END_TRANSACTION, SagaEndedEvent.class)
+ ));
+
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ }
+
+ @Test
+ public void failFastIfSagaLogIsDown() throws Exception {
+ EventStore sagaLog = mock(EventStore.class);
+ saga = sagaFactory.createSaga(requestJson, sagaId, sagaLog, sagaDefinition);
+
+ doThrow(RuntimeException.class).when(sagaLog).offer(any(SagaStartedEvent.class));
+
+ saga.run();
+
+ verify(sagaLog).offer(any(SagaStartedEvent.class));
+ verify(transaction1, never()).send(anyString(), any(SagaResponse.class));
+ verify(transaction2, never()).send(anyString(), any(SagaResponse.class));
+
+ verify(compensation1, never()).send(request1.serviceName());
+ verify(compensation2, never()).send(request2.serviceName());
+ }
+
+ private Answer<SagaResponse> withAnswer(Callable<SagaResponse> callable) {
+ return invocationOnMock -> callable.call();
+ }
+
+ private EventEnvelope envelope(SagaEvent event) {
+ return new EventEnvelope(idGenerator.nextId(), event);
+ }
+
+ private SagaRequest request(String requestId,
+ String serviceName,
+ Transaction transaction,
+ Compensation compensation,
+ String... parentIds) {
+
+ return new SagaRequestImpl(requestId, serviceName, Operation.TYPE_REST, transaction, compensation, parentIds);
+ }
+
+ private SagaRequest request(String requestId,
+ String serviceName,
+ Transaction transaction,
+ Compensation compensation,
+ Fallback fallback) {
+
+ return new SagaRequestImpl(requestId, serviceName, Operation.TYPE_REST, transaction, compensation, fallback);
+ }
+
+ private HashSet<String> setOf(String requestId) {
+ return new HashSet<>(singletonList(requestId));
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActorTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActorTest.java
new file mode 100644
index 0000000..5823a06
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/CompletionCallbackActorTest.java
@@ -0,0 +1,138 @@
+/*
+ * 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.saga.core.actors;
+
+import static akka.actor.ActorRef.noSender;
+import static akka.actor.Props.empty;
+import static com.seanyinx.github.unit.scaffolding.Randomness.uniquify;
+import static org.awaitility.Awaitility.await;
+import static org.hamcrest.core.Is.is;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.actors.messages.AbortMessage;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.servicecomb.saga.core.FailedSagaResponse;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+import org.apache.servicecomb.saga.core.actors.messages.FailMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.testkit.javadsl.TestKit;
+
+public class CompletionCallbackActorTest {
+ private static final ActorSystem actorSystem = ActorSystem.create();
+
+ private final SagaResponse response = Mockito.mock(SagaResponse.class);
+ private final RequestActorContext context = new RequestActorContext(null);
+
+ private final ActorRef actor1 = someActor();
+ private final ActorRef actor2 = someActor();
+
+ @Before
+ public void setUp() throws Exception {
+ context.addActor(uniquify("requestId"), actor1);
+ context.addActor(uniquify("requestId"), actor2);
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ TestKit.shutdownActorSystem(actorSystem);
+ }
+
+ @Test
+ public void killAllOnTransactionComplete() throws Exception {
+ new TestKit(actorSystem) {{
+ CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+
+ ActorRef actor = actorSystem.actorOf(CompletionCallbackActor.props(future));
+
+ actor.tell(context, noSender());
+ actor.tell(new TransactMessage(null, response), noSender());
+
+ await().atMost(2, TimeUnit.SECONDS)
+ .until(() -> actor1.isTerminated() && actor2.isTerminated() && actor.isTerminated());
+
+ assertThat(future.get(), is(response));
+ }};
+ }
+
+ @Test
+ public void killAllOnCompensationComplete() throws Exception {
+ new TestKit(actorSystem) {{
+ CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+
+ ActorRef actor = actorSystem.actorOf(CompletionCallbackActor.props(future));
+
+ actor.tell(context, noSender());
+ actor.tell(new CompensateMessage(response), noSender());
+
+ await().atMost(2, TimeUnit.SECONDS)
+ .until(() -> actor1.isTerminated() && actor2.isTerminated() && actor.isTerminated());
+
+ assertThat(future.get(), is(response));
+ }};
+ }
+
+ @Test
+ public void killAllOnFailure() throws Exception {
+ new TestKit(actorSystem) {{
+ CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+
+ ActorRef actor = actorSystem.actorOf(CompletionCallbackActor.props(future));
+
+ actor.tell(context, noSender());
+ actor.tell(new FailMessage(new RuntimeException("oops")), noSender());
+
+ await().atMost(2, TimeUnit.SECONDS)
+ .until(() -> actor1.isTerminated() && actor2.isTerminated() && actor.isTerminated());
+
+ assertThat(future.get(), is(instanceOf(FailedSagaResponse.class)));
+ }};
+ }
+
+ @Test
+ public void tellLeafToCompensateOnAbort() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addActor(NoOpSagaRequest.SAGA_END_REQUEST.id(), getRef());
+ CompletableFuture<SagaResponse> future = new CompletableFuture<>();
+
+ ActorRef actor = actorSystem.actorOf(CompletionCallbackActor.props(future));
+
+ actor.tell(context, noSender());
+ AbortMessage message = new AbortMessage(new RuntimeException("oops"));
+ actor.tell(message, noSender());
+
+ CompensateMessage compensateMessage = (CompensateMessage) receiveOne(duration("2 seconds"));
+ assertThat(compensateMessage.response(), is(message.response()));
+ }};
+ }
+
+ private ActorRef someActor() {
+ return actorSystem.actorOf(empty());
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/EventContextImplTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/EventContextImplTest.java
new file mode 100644
index 0000000..cfd3788
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/EventContextImplTest.java
@@ -0,0 +1,93 @@
+/*
+ * 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.saga.core.actors;
+
+import static com.seanyinx.github.unit.scaffolding.Randomness.uniquify;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Mockito.when;
+
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.AbortRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensationRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactionRecoveryMessage;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.scalatest.junit.JUnitSuite;
+
+import akka.actor.ActorSystem;
+import akka.testkit.javadsl.TestKit;
+
+public class EventContextImplTest extends JUnitSuite {
+ private static final ActorSystem actorSystem = ActorSystem.create();
+
+ private final SagaRequest request = Mockito.mock(SagaRequest.class);
+ private final SagaResponse response = Mockito.mock(SagaResponse.class);
+
+ private final RequestActorContext context = new RequestActorContext(null);
+ private final EventContextImpl eventContext = new EventContextImpl(context);
+ private final String requestId = uniquify("requestId");
+
+ @Before
+ public void setUp() throws Exception {
+ when(request.id()).thenReturn(requestId);
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ TestKit.shutdownActorSystem(actorSystem);
+ }
+
+ @Test
+ public void sendTransactionRecoveryMessageToActor_OnTransactionEnd() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addActor(requestId, getRef());
+
+ eventContext.endTransaction(request, response);
+
+ TransactionRecoveryMessage message = (TransactionRecoveryMessage) receiveOne(duration("2 seconds"));
+ assertThat(message.response(), is(response));
+ }};
+ }
+
+ @Test
+ public void sendCompensationRecoveryMessageToActor_OnCompensationEnd() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addActor(requestId, getRef());
+
+ eventContext.compensateTransaction(request, response);
+
+ expectMsgClass(CompensationRecoveryMessage.class);
+ }};
+ }
+
+ @Test
+ public void sendAbortMessageToActor_OnAbort() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addActor(requestId, getRef());
+
+ eventContext.abortTransaction(request, response);
+
+ AbortRecoveryMessage message = ((AbortRecoveryMessage) receiveOne(duration("2 seconds")));
+ assertThat(message.response(), is(response));
+ }};
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilderTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilderTest.java
new file mode 100644
index 0000000..b3a2c14
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorBuilderTest.java
@@ -0,0 +1,173 @@
+/*
+ * 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.saga.core.actors;
+
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.SagaTask;
+import org.hamcrest.Matchers;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.scalatest.junit.JUnitSuite;
+
+import com.seanyinx.github.unit.scaffolding.Randomness;
+
+import org.apache.servicecomb.saga.core.CompositeSagaResponse;
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.testkit.javadsl.TestKit;
+
+@SuppressWarnings("unchecked")
+public class RequestActorBuilderTest extends JUnitSuite {
+ private static final ActorSystem actorSystem = ActorSystem.create();
+
+ private final String requestId1 = Randomness.uniquify("requestId1");
+ private final String requestId2 = Randomness.uniquify("requestId2");
+ private final String requestId3 = Randomness.uniquify("requestId3");
+ private final String taskId = "some task";
+
+ private final SagaRequest request1 = Mockito.mock(SagaRequest.class);
+ private final SagaRequest request2 = Mockito.mock(SagaRequest.class);
+ private final SagaRequest request3 = Mockito.mock(SagaRequest.class);
+
+ private final SagaResponse response1 = Mockito.mock(SagaResponse.class);
+ private final SagaResponse response2 = Mockito.mock(SagaResponse.class);
+ private final SagaResponse response3 = Mockito.mock(SagaResponse.class);
+
+ private final SagaRequest[] requests = {request1, request2, request3};
+
+ private final SagaTask task = Mockito.mock(SagaTask.class);
+ private final Map<String, SagaTask> tasks = new HashMap<>();
+
+ private final FromJsonFormat<Set<String>> childrenExtractor = Mockito.mock(FromJsonFormat.class);
+ private final RequestActorBuilder actorBuilder = new RequestActorBuilder(actorSystem, childrenExtractor);
+
+ @Before
+ public void setUp() throws Exception {
+ tasks.put(SagaTask.SAGA_START_TASK, task);
+ tasks.put(SagaTask.SAGA_END_TASK, task);
+ tasks.put(taskId, task);
+
+ when(request1.id()).thenReturn(requestId1);
+ when(request2.id()).thenReturn(requestId2);
+ when(request3.id()).thenReturn(requestId3);
+
+ when(request1.task()).thenReturn(taskId);
+ when(request2.task()).thenReturn(taskId);
+ when(request3.task()).thenReturn(taskId);
+
+ when(request1.parents()).thenReturn(new String[0]);
+ when(request2.parents()).thenReturn(new String[] {requestId1});
+ when(request3.parents()).thenReturn(new String[] {requestId1});
+
+ when(task.commit(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE)).thenReturn(
+ SagaResponse.EMPTY_RESPONSE);
+ when(task.commit(request1, SagaResponse.EMPTY_RESPONSE)).thenReturn(response1);
+ when(task.commit(request2, response1)).thenReturn(response2);
+ when(task.commit(request3, response1)).thenReturn(response3);
+
+ when(childrenExtractor.fromJson(anyString())).thenReturn(Collections.emptySet());
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ TestKit.shutdownActorSystem(actorSystem);
+ }
+
+ @Test
+ public void createOneActorPerRequest() throws Exception {
+ new TestKit(actorSystem) {{
+ ArgumentCaptor<SagaResponse> argumentCaptor = ArgumentCaptor.forClass(SagaResponse.class);
+ when(task.commit(eq(NoOpSagaRequest.SAGA_END_REQUEST), argumentCaptor.capture())).thenReturn(
+ SagaResponse.EMPTY_RESPONSE);
+
+ ActorRef root = actorBuilder.build(requests, tasks, getRef()).actorOf(NoOpSagaRequest.SAGA_START_REQUEST.id());
+
+ root.tell(new TransactMessage(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE), getRef());
+
+ List<SagaResponse> responses = receiveN(1, duration("2 seconds")).stream()
+ .map(o -> ((TransactMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, Matchers.containsInAnyOrder(SagaResponse.EMPTY_RESPONSE));
+
+ verify(task).commit(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE);
+ verify(task).commit(request1, SagaResponse.EMPTY_RESPONSE);
+ verify(task).commit(request2, response1);
+ verify(task).commit(request3, response1);
+ verify(task).commit(eq(NoOpSagaRequest.SAGA_END_REQUEST), any(SagaResponse.class));
+
+ SagaResponse response = argumentCaptor.getValue();
+ assertThat(response, instanceOf(CompositeSagaResponse.class));
+ assertThat(((CompositeSagaResponse) response).responses(),
+ containsInAnyOrder(response2, response3));
+ }};
+ }
+
+ @Test
+ public void compensateAllCompletedTransactions() throws Exception {
+ new TestKit(actorSystem) {{
+ ArgumentCaptor<SagaResponse> argumentCaptor = ArgumentCaptor.forClass(SagaResponse.class);
+ when(task.commit(eq(NoOpSagaRequest.SAGA_END_REQUEST), argumentCaptor.capture())).thenReturn(
+ SagaResponse.EMPTY_RESPONSE);
+
+ ActorRef root = actorBuilder.build(requests, tasks, getRef()).actorOf(NoOpSagaRequest.SAGA_START_REQUEST.id());
+
+ root.tell(new TransactMessage(NoOpSagaRequest.SAGA_START_REQUEST, SagaResponse.EMPTY_RESPONSE), getRef());
+
+ List<SagaResponse> responses = receiveN(1, duration("2 seconds")).stream()
+ .map(o -> ((TransactMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, Matchers.containsInAnyOrder(SagaResponse.EMPTY_RESPONSE));
+
+ CompensateMessage message = new CompensateMessage(response1);
+ getLastSender().tell(message, getRef());
+ expectMsg(message);
+
+ verify(task).compensate(NoOpSagaRequest.SAGA_START_REQUEST);
+ verify(task).compensate(request1);
+ verify(task).compensate(request2);
+ verify(task).compensate(request3);
+ verify(task).compensate(NoOpSagaRequest.SAGA_END_REQUEST);
+ }};
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorTest.java
new file mode 100644
index 0000000..776b5aa
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/actors/RequestActorTest.java
@@ -0,0 +1,399 @@
+/*
+ * 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.saga.core.actors;
+
+import static akka.actor.ActorRef.noSender;
+import static akka.actor.Props.empty;
+import static com.seanyinx.github.unit.scaffolding.Randomness.uniquify;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.core.IsInstanceOf.instanceOf;
+import static org.junit.Assert.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.apache.servicecomb.saga.core.CompositeSagaResponse;
+import org.apache.servicecomb.saga.core.FailedSagaResponse;
+import org.apache.servicecomb.saga.core.Operation;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaResponse;
+import org.apache.servicecomb.saga.core.SagaStartFailedException;
+import org.apache.servicecomb.saga.core.SagaTask;
+import org.apache.servicecomb.saga.core.TransactionFailedException;
+import org.apache.servicecomb.saga.core.actors.messages.AbortMessage;
+import org.apache.servicecomb.saga.core.actors.messages.CompensationRecoveryMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactMessage;
+import org.apache.servicecomb.saga.core.actors.messages.TransactionRecoveryMessage;
+import org.hamcrest.Matchers;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+import org.scalatest.junit.JUnitSuite;
+
+import org.apache.servicecomb.saga.core.actors.messages.CompensateMessage;
+import org.apache.servicecomb.saga.core.actors.messages.FailMessage;
+import org.apache.servicecomb.saga.core.application.interpreter.FromJsonFormat;
+import akka.actor.ActorRef;
+import akka.actor.ActorSystem;
+import akka.testkit.javadsl.TestKit;
+
+@SuppressWarnings("unchecked")
+public class RequestActorTest extends JUnitSuite {
+ private final String parentRequestId1 = uniquify("parentRequestId1");
+ private final String parentRequestId2 = uniquify("parentRequestId2");
+ private final String requestId = uniquify("requestId");
+
+ private final SagaTask task = Mockito.mock(SagaTask.class);
+ private final SagaRequest request = Mockito.mock(SagaRequest.class, "request");
+ private final SagaRequest request1 = Mockito.mock(SagaRequest.class, "request1");
+ private final SagaRequest request2 = Mockito.mock(SagaRequest.class, "request2");
+ private final SagaResponse response = Mockito.mock(SagaResponse.class);
+ private final FromJsonFormat<Set<String>> childrenExtractor = mock(FromJsonFormat.class);
+
+ private final RequestActorContext context = new RequestActorContext(childrenExtractor);
+
+ private final TransactionFailedException exception = new TransactionFailedException("oops");
+ private static final ActorSystem actorSystem = ActorSystem.create();
+ private final CompensateMessage compensateMessage = new CompensateMessage(new FailedSagaResponse(exception));
+
+ @Before
+ public void setUp() throws Exception {
+ when(childrenExtractor.fromJson(anyString())).thenReturn(emptySet());
+ when(request.id()).thenReturn(requestId);
+
+ when(request1.id()).thenReturn(parentRequestId1);
+ when(request2.id()).thenReturn(parentRequestId2);
+ }
+
+ @AfterClass
+ public static void tearDown() throws Exception {
+ TestKit.shutdownActorSystem(actorSystem);
+ }
+
+ @Test
+ public void tellNodeResponseToAllChildren() throws Exception {
+ new TestKit(actorSystem) {{
+ addChildren(getRef());
+
+ ActorRef parent = someActor();
+ context.addParent(requestId, parent);
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+ when(task.commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE)).thenReturn(response);
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), parent);
+
+ List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+ .map(o -> ((TransactMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, containsInAnyOrder(response, response));
+
+ verify(task).commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE);
+ }};
+ }
+
+ @Test
+ public void executeTransaction_OnlyWhenAllParentsResponsesAreReceived() throws Exception {
+ new TestKit(actorSystem) {{
+ addChildren(getRef());
+
+ ActorRef parent1 = someActor();
+ context.addParent(requestId, parent1);
+
+ ActorRef parent2 = someActor();
+ context.addParent(requestId, parent2);
+
+ ArgumentCaptor<SagaResponse> argumentCaptor = ArgumentCaptor.forClass(SagaResponse.class);
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1, parentRequestId2});
+ when(task.commit(eq(request), argumentCaptor.capture())).thenReturn(response);
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), parent1);
+ expectNoMsg(duration("500 milliseconds"));
+
+ actorRef.tell(new TransactMessage(request2, SagaResponse.EMPTY_RESPONSE), parent2);
+
+ List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+ .map(o -> ((TransactMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, containsInAnyOrder(response, response));
+
+ SagaResponse response = argumentCaptor.getValue();
+ assertThat(response, instanceOf(CompositeSagaResponse.class));
+ assertThat(((CompositeSagaResponse) response).responses(),
+ Matchers.containsInAnyOrder(SagaResponse.EMPTY_RESPONSE, Operation.SUCCESSFUL_SAGA_RESPONSE));
+ }};
+ }
+
+ @Test
+ public void tellAllRelativesToAbortOnError() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addChild(requestId, getRef());
+ context.addActor(requestId, getRef());
+
+ context.addParent(requestId, getRef());
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+ when(task.commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE)).thenThrow(exception);
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), getRef());
+
+ List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+ .map(o -> ((AbortMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, containsInAnyOrder(instanceOf(FailedSagaResponse.class), instanceOf(FailedSagaResponse.class)));
+ }};
+ }
+
+ @Test
+ public void tellAllRelativesExceptSenderToAbortOnAbort() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addChild(requestId, getRef());
+ context.addActor(requestId, getRef());
+
+ context.addParent(requestId, getRef());
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new AbortMessage(exception), someActor());
+
+ List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+ .map(o -> ((AbortMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, containsInAnyOrder(instanceOf(FailedSagaResponse.class), instanceOf(FailedSagaResponse.class)));
+
+ actorRef.tell(new AbortMessage(exception), someActor());
+ expectNoMsg(duration("500 milliseconds"));
+ }};
+ }
+
+ @Test
+ public void compensateIfTransactionIsCompleted() throws Exception {
+ new TestKit(actorSystem) {{
+ addChildren(someActor());
+ context.addParent(requestId, getRef());
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+ when(task.commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE)).thenReturn(response);
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), getRef());
+ actorRef.tell(new AbortMessage(exception), noSender());
+ actorRef.tell(compensateMessage, getRef());
+ actorRef.tell(compensateMessage, getRef());
+
+ expectMsg(duration("2 seconds"), compensateMessage);
+ verify(task).compensate(request);
+
+ // no duplicate compensation
+ reset(task);
+ actorRef.tell(compensateMessage, getRef());
+ actorRef.tell(compensateMessage, getRef());
+ expectNoMsg(duration("200 milliseconds"));
+ verify(task, never()).compensate(request);
+ }};
+ }
+
+ @Test
+ public void doNotCompensateIfTransactionIsNotCompleted() throws Exception {
+ new TestKit(actorSystem) {{
+ addChildren(someActor());
+ context.addParent(requestId, getRef());
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new AbortMessage(exception), noSender());
+ actorRef.tell(compensateMessage, getRef());
+ actorRef.tell(compensateMessage, getRef());
+
+ List<Object> responses = receiveN(2, duration("2 seconds"));
+ assertThat(responses, contains(instanceOf(AbortMessage.class), instanceOf(CompensateMessage.class)));
+ verify(task, never()).compensate(request);
+
+ // no duplicate compensation
+ reset(task);
+ actorRef.tell(compensateMessage, getRef());
+ actorRef.tell(compensateMessage, getRef());
+ expectNoMsg(duration("200 milliseconds"));
+ verify(task, never()).compensate(request);
+ }};
+ }
+
+ @Test
+ public void skipIfActorIsNotChosenByAnyParent() throws Exception {
+ when(childrenExtractor.fromJson(Operation.SUCCESSFUL_SAGA_RESPONSE.body())).thenReturn(singleton("none"));
+
+ new TestKit(actorSystem) {{
+ addChildren(getRef());
+ context.addParent(requestId, getRef());
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), getRef());
+
+ List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+ .map(o -> ((TransactMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, Matchers.containsInAnyOrder(SagaResponse.NONE_RESPONSE, SagaResponse.NONE_RESPONSE));
+ verify(task, never()).commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE);
+
+ // skip compensation for ignored actor
+ actorRef.tell(compensateMessage, getRef());
+ actorRef.tell(compensateMessage, getRef());
+
+ expectMsg(duration("2 seconds"), compensateMessage);
+ verify(task, never()).compensate(request);
+ }};
+ }
+
+ @Test
+ public void transactIfChosenByAnyParent() throws Exception {
+ when(childrenExtractor.fromJson(Operation.SUCCESSFUL_SAGA_RESPONSE.body())).thenReturn(singleton(requestId));
+
+ new TestKit(actorSystem) {{
+ addChildren(getRef());
+
+ ActorRef parent1 = someActor();
+ context.addParent(requestId, parent1);
+
+ ActorRef parent2 = someActor();
+ context.addParent(requestId, parent2);
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1, parentRequestId2});
+ when(task.commit(eq(request), any(CompositeSagaResponse.class))).thenReturn(response);
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), parent1);
+ actorRef.tell(new TransactMessage(request2, Operation.SUCCESSFUL_SAGA_RESPONSE), parent2);
+
+ List<SagaResponse> responses = receiveN(2, duration("2 seconds")).stream()
+ .map(o -> ((TransactMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, containsInAnyOrder(response, response));
+ }};
+ }
+
+ @Test
+ public void tellTransactionResponseToChildrenOnRecovery() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addChild(requestId, getRef());
+
+ ActorRef parent = someActor();
+ context.addParent(requestId, parent);
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new TransactionRecoveryMessage(response), noSender());
+ actorRef.tell(new TransactMessage(request1, Operation.SUCCESSFUL_SAGA_RESPONSE), parent);
+
+ List<SagaResponse> responses = receiveN(1, duration("2 seconds")).stream()
+ .map(o -> ((TransactMessage) o).response())
+ .collect(Collectors.toList());
+
+ assertThat(responses, containsInAnyOrder(response));
+
+ verify(task, never()).commit(request, Operation.SUCCESSFUL_SAGA_RESPONSE);
+ }};
+ }
+
+ @Test
+ public void tellCompensationToParentsOnRecovery() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addChild(requestId, someActor());
+ context.addParent(requestId, getRef());
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new AbortMessage(exception), someActor());
+ actorRef.tell(new CompensationRecoveryMessage(), someActor());
+ actorRef.tell(compensateMessage, someActor());
+
+ List<Object> responses = receiveN(2, duration("2 seconds"));
+ assertThat(responses, contains(instanceOf(AbortMessage.class), instanceOf(CompensateMessage.class)));
+
+ verify(task, never()).compensate(request);
+ }};
+ }
+
+ @Test
+ public void abortOnPersistenceFailure() throws Exception {
+ new TestKit(actorSystem) {{
+ context.addChild(requestId, noSender());
+ context.addParent(requestId, getRef());
+
+ when(request.parents()).thenReturn(new String[] {parentRequestId1});
+
+ SagaStartFailedException oops = new SagaStartFailedException("oops", exception);
+ when(task.commit(request, SagaResponse.EMPTY_RESPONSE)).thenThrow(oops);
+
+ ActorRef actorRef = actorSystem.actorOf(RequestActor.props(context, task, request));
+
+ actorRef.tell(new TransactMessage(request, SagaResponse.EMPTY_RESPONSE), getRef());
+
+ expectMsgClass(FailMessage.class);
+ }};
+ }
+
+ private ActorRef someActor() {
+ return actorSystem.actorOf(empty());
+ }
+
+ private void addChildren(ActorRef ref) {
+ context.addChild(requestId, ref);
+ context.addChild(requestId, ref);
+ context.addActor(requestId, ref);
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/DirectedAcyclicGraphTraversalTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/DirectedAcyclicGraphTraversalTest.java
new file mode 100644
index 0000000..beb9607
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/DirectedAcyclicGraphTraversalTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.saga.core.dag;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.junit.Assert.assertThat;
+
+import java.util.Collection;
+import org.junit.Before;
+import org.junit.Test;
+
+public class DirectedAcyclicGraphTraversalTest {
+
+ private final Node<String> root = new Node<>(0, "i don't care");
+ private final Node<String> node1 = new Node<>(1, "i don't care");
+ private final Node<String> node2 = new Node<>(2, "i don't care");
+ private final Node<String> node3 = new Node<>(3, "i don't care");
+ private final Node<String> node4 = new Node<>(4, "i don't care");
+ private final Node<String> node5 = new Node<>(5, "i don't care");
+ private final Node<String> leaf = new Node<>(6, "i don't care");
+
+ private final SingleLeafDirectedAcyclicGraph<String> dag = new SingleLeafDirectedAcyclicGraph<>(root, leaf);
+
+ // 0
+ // / \
+ // 1 \
+ // / \ \
+ // 3 4 2
+ // \ / /
+ // 5 /
+ // \ /
+ // 6
+ @Before
+ public void setUp() throws Exception {
+ root.addChildren(asList(node1, node2));
+ node1.addChildren(asList(node3, node4));
+ node3.addChild(node5);
+ node4.addChild(node5);
+ node5.addChild(leaf);
+ node2.addChild(leaf);
+ }
+
+ @Test
+ public void traverseGraphOneLevelPerStepFromRoot() {
+ Traveller<String> traveller = new ByLevelTraveller<>(dag, new FromRootTraversalDirection<>());
+
+ Collection<Node<String>> nodes = traveller.nodes();
+
+ traveller.next();
+ assertThat(nodes, contains(root));
+
+ traveller.next();
+ assertThat(nodes, contains(root, node1, node2));
+
+ traveller.next();
+ assertThat(nodes, contains(root, node1, node2, node3, node4));
+
+ traveller.next();
+ assertThat(nodes, contains(root, node1, node2, node3, node4, node5));
+
+ traveller.next();
+ assertThat(nodes, contains(root, node1, node2, node3, node4, node5, leaf));
+ }
+
+ @Test
+ public void traverseGraphOneLevelPerStepFromLeaf() {
+ Traveller<String> traveller = new ByLevelTraveller<>(dag, new FromLeafTraversalDirection<>());
+
+ Collection<Node<String>> nodes = traveller.nodes();
+
+ traveller.next();
+ assertThat(nodes, contains(leaf));
+
+ traveller.next();
+ assertThat(nodes, contains(leaf, node2, node5));
+
+ traveller.next();
+ assertThat(nodes, contains(leaf, node2, node5, node3, node4));
+
+ traveller.next();
+ assertThat(nodes, contains(leaf, node2, node5, node3, node4, node1));
+
+ traveller.next();
+ assertThat(nodes, contains(leaf, node2, node5, node3, node4, node1, root));
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaExecutionComponentTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaExecutionComponentTest.java
new file mode 100644
index 0000000..8515f93
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBasedSagaExecutionComponentTest.java
@@ -0,0 +1,33 @@
+/*
+ * 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.saga.core.dag;
+
+import java.util.concurrent.Executors;
+
+import org.apache.servicecomb.saga.core.PersistentStore;
+import org.apache.servicecomb.saga.core.SagaExecutionComponentTestBase;
+import org.apache.servicecomb.saga.core.application.SagaFactory;
+
+
+public class GraphBasedSagaExecutionComponentTest extends SagaExecutionComponentTestBase {
+
+ @Override
+ protected SagaFactory sagaFactory(PersistentStore eventStore) {
+ return new GraphBasedSagaFactory(500, eventStore, null, Executors.newFixedThreadPool(5));
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBuilderTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBuilderTest.java
new file mode 100644
index 0000000..8703c7a
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphBuilderTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.saga.core.dag;
+
+import static com.seanyinx.github.unit.scaffolding.AssertUtils.expectFailing;
+import static org.apache.servicecomb.saga.core.Operation.TYPE_REST;
+import static java.util.Collections.emptyMap;
+import static java.util.Collections.emptySet;
+import static java.util.Collections.singleton;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.when;
+
+import java.util.Collection;
+import java.util.stream.Collectors;
+
+import org.apache.servicecomb.saga.core.NoOpSagaRequest;
+import org.apache.servicecomb.saga.core.SagaException;
+import org.apache.servicecomb.saga.core.SagaRequest;
+import org.apache.servicecomb.saga.core.SagaRequestImpl;
+import org.hamcrest.collection.IsIterableContainingInOrder;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.servicecomb.saga.core.CompensationImpl;
+import org.apache.servicecomb.saga.core.TransactionImpl;
+
+@SuppressWarnings("unchecked")
+public class GraphBuilderTest {
+
+ private final SagaRequest request1 = new SagaRequestImpl(
+ "request-aaa",
+ "aaa",
+ TYPE_REST,
+ new TransactionImpl("/rest/as", "post", emptyMap()),
+ new CompensationImpl("/rest/as","delete", emptyMap())
+ );
+
+ private final SagaRequest request2 = new SagaRequestImpl(
+ "request-bbb",
+ "bbb",
+ TYPE_REST,
+ new TransactionImpl("/rest/bs", "post", emptyMap()),
+ new CompensationImpl("/rest/bs","delete", emptyMap())
+ );
+
+ private final SagaRequest request3 = new SagaRequestImpl(
+ "request-ccc",
+ "ccc",
+ TYPE_REST,
+ new TransactionImpl("/rest/cs", "post", emptyMap()),
+ new CompensationImpl("/rest/cs","delete", emptyMap()),
+ null,
+ new String[]{"request-aaa", "request-bbb"}
+ );
+ private final SagaRequest[] requests = {request1, request2, request3};
+
+ private final SagaRequest duplicateRequest = new SagaRequestImpl(
+ "request-duplicate-id",
+ "xxx",
+ TYPE_REST,
+ new TransactionImpl("/rest/xs", "post", emptyMap()),
+ new CompensationImpl("/rest/xs","delete", emptyMap())
+ );
+ private final SagaRequest[] duplicateRequests = {duplicateRequest, duplicateRequest};
+
+ private final GraphCycleDetector<SagaRequest> detector = Mockito.mock(GraphCycleDetector.class);
+ private final GraphBuilder graphBuilder = new GraphBuilder(detector);
+
+ @Before
+ public void setUp() throws Exception {
+ when(detector.cycleJoints(any())).thenReturn(emptySet());
+ }
+
+ @Test
+ public void buildsGraphOfParallelRequests() {
+ SingleLeafDirectedAcyclicGraph<SagaRequest> tasks = graphBuilder.build(requests);
+
+ Traveller<SagaRequest> traveller = new ByLevelTraveller<>(tasks, new FromRootTraversalDirection<>());
+ Collection<Node<SagaRequest>> nodes = traveller.nodes();
+
+ traveller.next();
+ assertThat(requestsOf(nodes), IsIterableContainingInOrder.contains(NoOpSagaRequest.SAGA_START_REQUEST));
+ nodes.clear();
+
+ traveller.next();
+ assertThat(requestsOf(nodes), contains(request1, request2));
+ nodes.clear();
+
+ traveller.next();
+ assertThat(requestsOf(nodes), contains(request3));
+ nodes.clear();
+
+ traveller.next();
+ assertThat(requestsOf(nodes), IsIterableContainingInOrder.contains(NoOpSagaRequest.SAGA_END_REQUEST));
+ }
+
+ @Test
+ public void blowsUpWhenJsonContainsDuplicateRequestId() {
+ try {
+ graphBuilder.build(duplicateRequests);
+ fail(SagaException.class.getSimpleName() + " is expected, but none thrown");
+ } catch (SagaException e) {
+ assertThat(e.getMessage(),
+ is("Failed to interpret requests with duplicate request id: request-duplicate-id"));
+ }
+ }
+
+ @Test
+ public void blowsUpWhenGraphContainsCycle() {
+ reset(detector);
+ when(detector.cycleJoints(any())).thenReturn(singleton(new Node<>(0L, null)));
+
+ try {
+ graphBuilder.build(requests);
+ expectFailing(SagaException.class);
+ } catch (SagaException e) {
+ assertThat(e.getMessage(), startsWith("Cycle detected in the request graph at nodes "));
+ }
+ }
+
+ private Collection<SagaRequest> requestsOf(Collection<Node<SagaRequest>> nodes) {
+ return nodes.stream()
+ .map(Node::value)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorTest.java
new file mode 100644
index 0000000..934c54b
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/GraphCycleDetectorTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.saga.core.dag;
+
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import java.util.Set;
+import org.junit.Before;
+import org.junit.Test;
+
+@SuppressWarnings("unchecked")
+public class GraphCycleDetectorTest {
+
+ private final Node<String> root = node(0);
+ private final Node<String> leaf = node(Long.MAX_VALUE);
+
+ private final Node<String> node1 = node(1);
+ private final Node<String> node2 = node(2);
+ private final Node<String> node3 = node(3);
+
+ private final SingleLeafDirectedAcyclicGraph<String> graph = new SingleLeafDirectedAcyclicGraph<>(root, leaf);
+ private final GraphCycleDetector<String> detector = new GraphCycleDetectorImpl<>();
+
+ @Before
+ public void setUp() throws Exception {
+ root.addChild(node1);
+
+ node2.addChild(leaf);
+ node3.addChild(leaf);
+ }
+
+ @Test
+ public void emptyNodesWhenNoCycleInGraph() {
+ node1.addChild(node2);
+ node1.addChild(node3);
+
+ Set<Node<String>> nodes = detector.cycleJoints(graph);
+
+ assertThat(nodes.isEmpty(), is(true));
+ }
+
+ @Test
+ public void nonEmptyNodesIfGraphContainsCycle() {
+ node1.addChild(node2);
+ node2.addChild(node3);
+ node3.addChild(node1);
+
+ Set<Node<String>> nodes = detector.cycleJoints(graph);
+
+ assertThat(nodes, contains(node1));
+ }
+
+ private Node<String> node(long id) {
+ return new Node<>(id, "value " + id);
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/NodeTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/NodeTest.java
new file mode 100644
index 0000000..2da4ba0
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/core/dag/NodeTest.java
@@ -0,0 +1,80 @@
+/*
+ * 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.saga.core.dag;
+
+import static java.util.Arrays.asList;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
+import static org.hamcrest.core.Is.is;
+import static org.junit.Assert.assertThat;
+
+import org.junit.Before;
+import org.junit.Test;
+
+public class NodeTest {
+
+
+ private final Node<String> parent = new Node<>(0, "i don't care");
+ private final Node<String> node1 = new Node<>(1, "i don't care");
+ private final Node<String> node2 = new Node<>(2, "i don't care");
+ private final Node<String> node3 = new Node<>(3, "i don't care");
+ private final Node<String> node4 = new Node<>(4, "i don't care");
+ private final Node<String> node5 = new Node<>(5, "i don't care");
+ private final Node<String> node6 = new Node<>(6, "i don't care");
+
+ // 0
+ // / \
+ // 1 \
+ // / \ \
+ // 3 4 2
+ // \ / /
+ // 5 /
+ // \ /
+ // 6
+ @Before
+ public void setUp() throws Exception {
+ parent.addChildren(asList(node1, node2));
+ node1.addChildren(asList(node3, node4));
+ node3.addChild(node5);
+ node4.addChild(node5);
+ node5.addChild(node6);
+ node2.addChild(node6);
+ }
+
+ @Test
+ public void nodeIsLinkedBidirectionally() {
+ assertThat(parent.children(), containsInAnyOrder(node1, node2));
+ assertThat(node1.parents(), contains(parent));
+ assertThat(node1.children(), containsInAnyOrder(node3, node4));
+
+ assertThat(node2.parents(), contains(parent));
+ assertThat(node2.children(), contains(node6));
+
+ assertThat(node3.parents(), contains(node1));
+ assertThat(node3.children(), contains(node5));
+
+ assertThat(node4.parents(), contains(node1));
+ assertThat(node4.children(), contains(node5));
+
+ assertThat(node5.parents(), containsInAnyOrder(node3, node4));
+ assertThat(node5.children(), contains(node6));
+
+ assertThat(node6.parents(), containsInAnyOrder(node2, node5));
+ assertThat(node6.children().isEmpty(), is(true));
+ }
+}
diff --git a/saga-core/src/test/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStoreTest.java b/saga-core/src/test/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStoreTest.java
new file mode 100644
index 0000000..1216e5f
--- /dev/null
+++ b/saga-core/src/test/java/org/apache/servicecomb/saga/infrastructure/ContextAwareEventStoreTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.saga.infrastructure;
+
+import static org.mockito.Mockito.verify;
+
+import org.apache.servicecomb.saga.core.EventStore;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import org.apache.servicecomb.saga.core.SagaContext;
+import org.apache.servicecomb.saga.core.SagaEvent;
+
+public class ContextAwareEventStoreTest {
+ private final EventStore underlying = Mockito.mock(EventStore.class);
+ private final SagaContext context = Mockito.mock(SagaContext.class);
+ private final SagaEvent sagaEvent = Mockito.mock(SagaEvent.class);
+
+ private final ContextAwareEventStore contextAwareEventStore = new ContextAwareEventStore(underlying, context);
+
+ @Test
+ public void persistWithUnderlyingStore() throws Exception {
+ contextAwareEventStore.offer(sagaEvent);
+
+ verify(sagaEvent).gatherTo(context);
+ verify(underlying).offer(sagaEvent);
+ }
+}
diff --git a/saga-core/src/test/resources/application.conf b/saga-core/src/test/resources/application.conf
new file mode 100644
index 0000000..0629dd6
--- /dev/null
+++ b/saga-core/src/test/resources/application.conf
@@ -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.
+## ---------------------------------------------------------------------------
+akka {
+ loggers = ["akka.event.slf4j.Slf4jLogger"]
+ loglevel = "DEBUG"
+ logging-filter = "akka.event.slf4j.Slf4jLoggingFilter"
+}
diff --git a/saga-core/src/test/resources/log4j2.xml b/saga-core/src/test/resources/log4j2.xml
new file mode 100644
index 0000000..cae04cb
--- /dev/null
+++ b/saga-core/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/saga-demo/conditional-transaction-demo/README.md b/saga-demo/conditional-transaction-demo/README.md
new file mode 100755
index 0000000..9ae1deb
--- /dev/null
+++ b/saga-demo/conditional-transaction-demo/README.md
@@ -0,0 +1,174 @@
+# Conditional Transaction Demo
+This demo simulates a partial backend of an e-commerce application including four services:
+* payment
+* membership
+* inventory
+* supplier
+
+**Note** Please go through the dependency-free-transaction-demo first, before proceeding with this demo.
+
+## Background
+Many e-commerce sites have membership concept, which will level up when a customer makes a certain amount of purchase.
+Moreover, as goods are sold and the stock drops to a certain level, the e-commerce company needs to fetch more goods from
+its suppliers. Both scenarios introduce conditional transactions.
+
+## Workflow
+```
+ /---> if total purchase >= 1K, level up membership
+start ---> make payment ---> dispatch product from inventory ---> end
+ \---> if product stock < 10, replenish product from supplier
+```
+The conditions are business logic and belong to business service. Therefore, the decision to level up membership is made
+in payment service and the decision to replenish products is made in inventory service. Saga is to be informed of service
+decision by adding an additional element `sagaChildren` in service response JSON to indicate which services are to be invoked
+next as shown below.
+```json
+{
+ "customerId": "mike",
+ "body": "Payment made with id xxx for customer mike",
+ "sagaChildren": ["inventory"]
+}
+```
+
+If `sagaChildren` is empty or not provided in service response, saga will invoke all of its child services. To invoke none
+of them, explicitly return `"sagaChildren": ["none"]`.
+
+If any of the sub-transaction specified in sagaChildren fails, all completed sub-transactions will be compensated as usual,
+when the recovery policy if backward recovery.
+
+## Running Demo
+1. run the following command to create docker images in saga project root folder.
+```
+mvn package -DskipTests -Pdocker -Pdemo
+```
+
+2. start application up in saga/saga-demo/conditional-transaction-demo with the following command
+```
+docker-compose up
+```
+
+## User Requests
+The request JSON to ensure the workflow order looks like the following:
+```json
+{
+ "policy": "BackwardRecovery",
+ "requests": [
+ {
+ "id": "payment",
+ "type": "rest",
+ "serviceName": "payment.servicecomb.io:8080",
+ "transaction": {
+ "method": "post",
+ "path": "/payment",
+ "params": {
+ "form": {
+ "customerId": "mike",
+ "purchaseAmount": 400
+ }
+ }
+ },
+ "compensation": {
+ "method": "put",
+ "path": "/payment",
+ "params": {
+ "form": {
+ "customerId": "mike"
+ }
+ }
+ }
+ },
+ {
+ "id": "membership",
+ "type": "rest",
+ "serviceName": "membership.servicecomb.io:8080",
+ "parents": [
+ "payment"
+ ],
+ "transaction": {
+ "method": "post",
+ "path": "/membership",
+ "params": {
+ "form": {
+ "customerId": "mike"
+ }
+ }
+ },
+ "compensation": {
+ "method": "put",
+ "path": "/membership",
+ "params": {
+ "form": {
+ "customerId": "mike"
+ }
+ }
+ }
+ },
+ {
+ "id": "inventory",
+ "type": "rest",
+ "serviceName": "inventory.servicecomb.io:8080",
+ "parents": [
+ "payment"
+ ],
+ "transaction": {
+ "method": "post",
+ "path": "/inventory",
+ "params": {
+ "form": {
+ "customerId": "mike"
+ }
+ }
+ },
+ "compensation": {
+ "method": "put",
+ "path": "/inventory",
+ "params": {
+ "form": {
+ "customerId": "mike"
+ }
+ }
+ }
+ },
+ {
+ "id": "supplier",
+ "type": "rest",
+ "serviceName": "supplier.servicecomb.io:8080",
+ "parents": [
+ "inventory"
+ ],
+ "transaction": {
+ "method": "post",
+ "path": "/supplier",
+ "params": {
+ "form": {
+ "customerId": "servicecomb_mall"
+ }
+ }
+ },
+ "compensation": {
+ "method": "put",
+ "path": "/supplier",
+ "params": {
+ "form": {
+ "customerId": "servicecomb_mall"
+ }
+ }
+ }
+ }
+ ]
+}
+
+```
+
+To send the above JSON request to Saga, use [postman](https://www.getpostman.com/postman) with POST request to url `http://<docker.host.ip>:8083/requests`
+
+Each request to payment service will increase user mike's total purchase by $400. The 3<sup>rd</sup> request will trigger
+membership level up.
+
+The initial product stock is 11 in inventory and each request to inventory service will deduct product stock by 1.
+So the 2<sup>nd</sup> request will trigger product replenishment from supplier.
+
+**Note** transactions and compensations implemented by services must be idempotent. In this demo, we did not enforce that
+for simplicity.
+
+To see all events generated by Saga, visit `http://<docker.host.ip>:8083/events` with postman.
diff --git a/saga-demo/conditional-transaction-demo/conditional-transaction-demo-tests/pom.xml b/saga-demo/conditional-transaction-demo/conditional-transaction-demo-tests/pom.xml
new file mode 100644
index 0000000..57a3895
--- /dev/null
+++ b/saga-demo/conditional-transaction-demo/conditional-transaction-demo-tests/pom.xml
@@ -0,0 +1,301 @@
+<?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>conditional-transaction-demo</artifactId>
+ <groupId>org.apache.servicecomb.saga.demo</groupId>
+ <version>0.3.0-SNAPSHOT</version>
... 11732 lines suppressed ...