mirror of https://github.com/dapr/quickstarts.git
adding WIP resiliency and compensation & combined-patterns
Signed-off-by: salaboy <Salaboy@gmail.com>
This commit is contained in:
parent
f31aaccd1d
commit
fa3d4f1d55
|
|
@ -0,0 +1,141 @@
|
|||
# Combined Workflow Patterns
|
||||
|
||||
This tutorial demonstrates how several workflow patterns can be combined in a single, more realistic, workflow. Some of the workflow activities are using other Dapr APIs, such as state management, service invocation, and Pub/Sub.
|
||||
|
||||
## Inspect the code
|
||||
|
||||
The demo consist of two applications:
|
||||
|
||||
- `WorkflowApp` is the main application that orchestrates an order process in the `OrderWorkflow`.
|
||||
- `ShippingApp` is a supporting service that is being called by the `OrderWorkflow`.
|
||||
|
||||
The `OrderWorkflow` combines task chaining, fan-out/fan-in, and waiting for external event patterns. The workflow contains a number of activities for order processing including checking inventory, register shipment, process payment and more with a final order status being returned with the results of the order. It uses compensating logic in case the shipment fails to get registered and the customer needs to be reimbursed for the payment.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
SW((Start
|
||||
Workflow))
|
||||
EW((End
|
||||
Workflow))
|
||||
subgraph OrderWorkflow
|
||||
direction LR
|
||||
CHKI[Check inventory]
|
||||
CHKD[Check shipping
|
||||
destination]
|
||||
IF1{Success?}
|
||||
PAY[Process
|
||||
payment]
|
||||
UPD[Update
|
||||
inventory]
|
||||
REG[Register
|
||||
shipment]
|
||||
WAIT[Wait for
|
||||
confirmation]
|
||||
IF2{Success?}
|
||||
RI[Reimburse
|
||||
customer]
|
||||
end
|
||||
subgraph Shipping
|
||||
direction LR
|
||||
REG2[registerShipment]
|
||||
CHKD2[checkDestination]
|
||||
end
|
||||
SW --> CHKI
|
||||
SW --> CHKD <--> CHKD2
|
||||
CHKI --> IF1
|
||||
CHKD --> IF1
|
||||
IF1 --> PAY
|
||||
PAY --> UPD
|
||||
UPD --> REG -.->|pub/sub| REG2
|
||||
REG2 -.->|pub/sub| WAIT
|
||||
REG --> WAIT
|
||||
WAIT --> IF2
|
||||
IF2 -->|Yes| EW
|
||||
IF2 -->|No| RI
|
||||
RI --> EW
|
||||
```
|
||||
|
||||
## Run the tutorial
|
||||
|
||||
1. Use a terminal to navigate to the `tutorials/workflow/csharp/combined-patterns` folder.
|
||||
2. Build the projects using the .NET CLI.
|
||||
|
||||
```bash
|
||||
dotnet build ./WorkflowApp/
|
||||
dotnet build ./ShippingApp/
|
||||
```
|
||||
|
||||
3. Use the Dapr CLI to run the Dapr Multi-App run file. This starts both applications `order-workflow` and `shipping` with the Dapr components in the [resources](./resources) folder.
|
||||
|
||||
<!-- STEP
|
||||
name: Run multi app run template
|
||||
expected_stdout_lines:
|
||||
- 'Started Dapr with app id "order-workflow"'
|
||||
- 'Started Dapr with app id "shipping"'
|
||||
expected_stderr_lines:
|
||||
working_dir: .
|
||||
output_match_mode: substring
|
||||
background: true
|
||||
sleep: 15
|
||||
timeout_seconds: 30
|
||||
-->
|
||||
```bash
|
||||
dapr run -f .
|
||||
```
|
||||
<!-- END_STEP -->
|
||||
|
||||
4. Use the POST request in the [`order-workflow.http`](./order-workflow.http) file to start the workflow, or use this cURL command:
|
||||
|
||||
```bash
|
||||
curl -i --request POST \
|
||||
--url http://localhost:5260/start \
|
||||
--header 'content-type: application/json' \
|
||||
--data '{"id": "b0d38481-5547-411e-ae7b-255761cce17a","orderItem" : {"productId": "RBD001","productName": "Rubber Duck","quantity": 10,"totalPrice": 15.00},"customerInfo" : {"id" : "Customer1","country" : "The Netherlands"}}'
|
||||
```
|
||||
|
||||
The input for the workflow is an `Order` object:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "{{orderId}}",
|
||||
"orderItem" : {
|
||||
"productId": "RBD001",
|
||||
"productName": "Rubber Duck",
|
||||
"quantity": 10,
|
||||
"totalPrice": 15.00
|
||||
},
|
||||
"customerInfo" : {
|
||||
"id" : "Customer1",
|
||||
"country" : "The Netherlands"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The app logs should come from both services executing all activities as follows:
|
||||
|
||||
```text
|
||||
== APP - order-workflow == CheckInventory: Received input: OrderItem { ProductId = RBD001, ProductName = Rubber Duck, Quantity = 10, TotalPrice = 15.00 }.
|
||||
== APP - order-workflow == CheckShippingDestination: Received input: Order { Id = 06d49c54-bf65-427b-90d1-730987e96e61, OrderItem = OrderItem { ProductId = RBD001, ProductName = Rubber Duck, Quantity = 10, TotalPrice = 15.00 }, CustomerInfo = CustomerInfo { Id = Customer1, Country = The Netherlands } }.
|
||||
== APP - shipping == checkDestination: Received input: Order { Id = 06d49c54-bf65-427b-90d1-730987e96e61, OrderItem = OrderItem { ProductId = RBD001, ProductName = Rubber Duck, Quantity = 10, TotalPrice = 15.00 }, CustomerInfo = CustomerInfo { Id = Customer1, Country = The Netherlands } }.
|
||||
== APP - order-workflow == ProcessPayment: Received input: Order { Id = 06d49c54-bf65-427b-90d1-730987e96e61, OrderItem = OrderItem { ProductId = RBD001, ProductName = Rubber Duck, Quantity = 10, TotalPrice = 15.00 }, CustomerInfo = CustomerInfo { Id = Customer1, Country = The Netherlands } }.
|
||||
== APP - order-workflow == UpdateInventory: Received input: OrderItem { ProductId = RBD001, ProductName = Rubber Duck, Quantity = 10, TotalPrice = 15.00 }.
|
||||
== APP - order-workflow == RegisterShipment: Received input: Order { Id = 06d49c54-bf65-427b-90d1-730987e96e61, OrderItem = OrderItem { ProductId = RBD001, ProductName = Rubber Duck, Quantity = 10, TotalPrice = 15.00 }, CustomerInfo = CustomerInfo { Id = Customer1, Country = The Netherlands } }.
|
||||
== APP - shipping == registerShipment: Received input: Order { Id = 06d49c54-bf65-427b-90d1-730987e96e61, OrderItem = OrderItem { ProductId = RBD001, ProductName = Rubber Duck, Quantity = 10, TotalPrice = 15.00 }, CustomerInfo = CustomerInfo { Id = Customer1, Country = The Netherlands } }.
|
||||
== APP - order-workflow == Shipment registered for order ShipmentRegistrationStatus { OrderId = 06d49c54-bf65-427b-90d1-730987e96e61, IsSuccess = True, Message = }
|
||||
```
|
||||
|
||||
5. Use the GET request in the [`order-workflow.http`](./order-workflow.http) file to get the status of the workflow, or use this cURL command:
|
||||
|
||||
```bash
|
||||
curl --request GET --url http://localhost:3560/v1.0/workflows/dapr/06d49c54-bf65-427b-90d1-730987e96e61
|
||||
```
|
||||
|
||||
The expected serialized output of the workflow is:
|
||||
|
||||
```txt
|
||||
{\"IsSuccess\":true,\"Message\":\"Order 06d49c54-bf65-427b-90d1-730987e96e61 processed successfully.\"}"
|
||||
```
|
||||
|
||||
*The Order ID is generated when making the request and is different each time.*
|
||||
|
||||
6. Stop the Dapr Multi-App run process by pressing `Ctrl+C`.
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
@apphost=http://localhost:8080
|
||||
|
||||
### Start the OrderWorkflow workflow
|
||||
# @name startWorkflowRequest
|
||||
@orderId={{$guid}}
|
||||
POST {{ apphost }}/start
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"id": "{{orderId}}",
|
||||
"orderItem" : {
|
||||
"productId": "RBD001",
|
||||
"productName": "Rubber Duck",
|
||||
"quantity": 10,
|
||||
"totalPrice": 15.00
|
||||
},
|
||||
"customerInfo" : {
|
||||
"id" : "Customer1",
|
||||
"country" : "The Netherlands"
|
||||
}
|
||||
}
|
||||
|
||||
### Get the workflow status
|
||||
@instanceId={{startWorkflowRequest.response.headers.Location}}
|
||||
GET {{ apphost }}/status/{{ instanceId }}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<artifactId>shipping-app</artifactId>
|
||||
<name>shipping-app</name>
|
||||
<description>Shipping App Example</description>
|
||||
|
||||
<properties>
|
||||
<dapr.spring.version>0.15.0-rc-7</dapr.spring.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dapr.spring</groupId>
|
||||
<artifactId>dapr-spring-boot-starter</artifactId>
|
||||
<version>${dapr.spring.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dapr.spring</groupId>
|
||||
<artifactId>dapr-spring-boot-starter-test</artifactId>
|
||||
<version>${dapr.spring.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
public class ExternalEventsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ExternalEventsApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class ExternalEventsConfiguration {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplateBuilder().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ObjectMapper mapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
|
||||
import io.dapr.spring.workflows.config.EnableDaprWorkflows;
|
||||
import io.dapr.springboot.examples.external.ApprovalStatus;
|
||||
import io.dapr.springboot.examples.external.ExternalEventsWorkflow;
|
||||
import io.dapr.springboot.examples.external.Order;
|
||||
import io.dapr.workflows.client.DaprWorkflowClient;
|
||||
import io.dapr.workflows.client.WorkflowInstanceStatus;
|
||||
import io.dapr.workflows.client.WorkflowRuntimeStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@RestController
|
||||
/*
|
||||
* Dapr Workflows and Activities need to be registered in the DI container otherwise
|
||||
* the Dapr runtime does not know this application contains workflows and activities.
|
||||
* In Spring Boot Applications, the `@EnableDaprWorkflow` annotation takes care of registering
|
||||
* all workflows and activities components found in the classpath.
|
||||
*/
|
||||
@EnableDaprWorkflows
|
||||
public class ExternalEventsRestController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ExternalEventsRestController.class);
|
||||
|
||||
@Autowired
|
||||
private DaprWorkflowClient daprWorkflowClient;
|
||||
|
||||
private String instanceId;
|
||||
|
||||
/**
|
||||
* The DaprWorkflowClient is the API to manage workflows.
|
||||
* Here it is used to schedule a new workflow instance.
|
||||
*
|
||||
* @return the instanceId of the ExternalEventsWorkflow execution
|
||||
*/
|
||||
@PostMapping("start")
|
||||
public String basic(@RequestBody Order order) throws TimeoutException {
|
||||
logger.info("Received order: {}", order);
|
||||
instanceId = daprWorkflowClient.scheduleNewWorkflow(ExternalEventsWorkflow.class, order);
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the status of the workflow
|
||||
*
|
||||
* @return the status of the ExternalEventsWorkflow instance
|
||||
*/
|
||||
@GetMapping("status")
|
||||
public String status() throws TimeoutException {
|
||||
if(instanceId != null && !instanceId.isEmpty()) {
|
||||
WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(instanceId, true);
|
||||
if (instanceState != null) {
|
||||
if (instanceState.getRuntimeStatus().equals(WorkflowRuntimeStatus.COMPLETED)) {
|
||||
var output = instanceState.readOutputAs(String.class);
|
||||
if (output != null && !output.isEmpty()) {
|
||||
return "Workflow Instance (" + instanceId + ") Status: " + instanceState.getRuntimeStatus().name() + "\n"
|
||||
+ "Output: " + output;
|
||||
}
|
||||
}
|
||||
return "Workflow Instance (" + instanceId + ") Status: " + instanceState.getRuntimeStatus().name();
|
||||
}
|
||||
}
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise a workflow event
|
||||
*/
|
||||
@PostMapping("event")
|
||||
public void raiseEvent(@RequestBody ApprovalStatus approvalStatus) {
|
||||
daprWorkflowClient.raiseEvent(instanceId, "approval-event", approvalStatus);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.dapr.springboot.examples.external;
|
||||
|
||||
public record ApprovalStatus(String orderId, boolean isApproved){}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.external;
|
||||
|
||||
import io.dapr.durabletask.TaskCanceledException;
|
||||
import io.dapr.springboot.examples.external.activities.ProcessOrderActivity;
|
||||
import io.dapr.springboot.examples.external.activities.SendNotificationActivity;
|
||||
import io.dapr.workflows.Workflow;
|
||||
import io.dapr.workflows.WorkflowStub;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Component
|
||||
public class ExternalEventsWorkflow implements Workflow {
|
||||
@Override
|
||||
public WorkflowStub create() {
|
||||
return ctx -> {
|
||||
ctx.getLogger().info("Starting Workflow: {}", ctx.getName());
|
||||
|
||||
var order = ctx.getInput(Order.class);
|
||||
var approvalStatus = new ApprovalStatus(order.id(), true);
|
||||
|
||||
if(order.totalPrice() > 250){
|
||||
try{
|
||||
approvalStatus = ctx.waitForExternalEvent("approval-event", Duration.ofSeconds(12), ApprovalStatus.class).await();
|
||||
} catch (TaskCanceledException tce){
|
||||
var notification = "Approval request for order " + order.id() + " timed out.";
|
||||
ctx.callActivity(SendNotificationActivity.class.getName(), notification).await();
|
||||
ctx.complete(notification);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (approvalStatus.isApproved()){
|
||||
ctx.callActivity(ProcessOrderActivity.class.getName(), order).await();
|
||||
}
|
||||
var notification = approvalStatus.isApproved() ?
|
||||
"Order " + order.id() + " has been approved." :
|
||||
"Order " + order.id() +" has been rejected.";
|
||||
|
||||
ctx.callActivity(SendNotificationActivity.class.getName(), notification).await();
|
||||
|
||||
ctx.complete(notification);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.dapr.springboot.examples.external;
|
||||
|
||||
public record Order(String id, String description, int quantity, double totalPrice){}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.external.activities;
|
||||
|
||||
import io.dapr.springboot.examples.external.Order;
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
@Component
|
||||
public class ProcessOrderActivity implements WorkflowActivity {
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(ProcessOrderActivity.class);
|
||||
var order = ctx.getInput(Order.class);
|
||||
// Imagine the order being processed by another system
|
||||
logger.info("{} : Processed Order: {}", ctx.getName(), order.id());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.external.activities;
|
||||
|
||||
import io.dapr.springboot.examples.external.Order;
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RequestApprovalActivity implements WorkflowActivity {
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(RequestApprovalActivity.class);
|
||||
var order = ctx.getInput(Order.class);
|
||||
logger.info("{} : Request Approval for Order: {}", ctx.getName(), order.id());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.external.activities;
|
||||
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
*/
|
||||
@Component
|
||||
public class SendNotificationActivity implements WorkflowActivity {
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(SendNotificationActivity.class);
|
||||
var message = ctx.getInput(String.class);
|
||||
|
||||
// Imagine a notification being sent to the user
|
||||
logger.info("{} : Sending Notification: {}", ctx.getName(), message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
spring.application.name=external-system-interactions
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import io.dapr.testcontainers.Component;
|
||||
import io.dapr.testcontainers.DaprContainer;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
public class DaprTestContainersConfig {
|
||||
|
||||
@Bean
|
||||
@ServiceConnection
|
||||
public DaprContainer daprContainer() {
|
||||
return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
|
||||
.withAppName("external-system-interactions")
|
||||
.withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true))))
|
||||
.withAppPort(8080)
|
||||
.withAppHealthCheckPath("/actuator/health")
|
||||
.withAppChannelAddress("host.testcontainers.internal");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import io.dapr.client.DaprClient;
|
||||
import io.dapr.springboot.DaprAutoConfiguration;
|
||||
import io.dapr.springboot.examples.external.ApprovalStatus;
|
||||
import io.dapr.springboot.examples.external.Order;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static io.dapr.springboot.examples.StringMatchesUUIDPattern.matchesThePatternOfAUUID;
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SpringBootTest(classes = {TestExternalEventsApplication.class, DaprTestContainersConfig.class,
|
||||
DaprAutoConfiguration.class, },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
class ExternalEventsAppTests {
|
||||
|
||||
@Autowired
|
||||
private DaprClient daprClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RestAssured.baseURI = "http://localhost:" + 8080;
|
||||
org.testcontainers.Testcontainers.exposeHostPorts(8080);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testExternalEventsWorkflow() throws InterruptedException {
|
||||
|
||||
var order = new Order("123", "Rubber ducks", 100, 500);
|
||||
given().contentType(ContentType.JSON)
|
||||
.body(order)
|
||||
.when()
|
||||
.post("/start")
|
||||
.then()
|
||||
.statusCode(200).body(matchesThePatternOfAUUID());
|
||||
|
||||
|
||||
String status = given().contentType(ContentType.JSON)
|
||||
.when()
|
||||
.get("/status")
|
||||
.then()
|
||||
.statusCode(200).extract().asString();
|
||||
|
||||
assertTrue(status.contains("RUNNING"));
|
||||
|
||||
ApprovalStatus approvalStatus = new ApprovalStatus("123", true);
|
||||
given().contentType(ContentType.JSON)
|
||||
.body(approvalStatus)
|
||||
.when()
|
||||
.post("/event")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
|
||||
// Wait for the workflow instance to complete
|
||||
|
||||
await().atMost(Duration.ofSeconds(2))
|
||||
.pollDelay(500, TimeUnit.MILLISECONDS)
|
||||
.pollInterval(500, TimeUnit.MILLISECONDS)
|
||||
.until(() -> {
|
||||
var completedStatus = given().contentType(ContentType.JSON)
|
||||
.when()
|
||||
.get("/status")
|
||||
.then()
|
||||
.statusCode(200).extract().asString();
|
||||
assertTrue(completedStatus.contains("COMPLETED"));
|
||||
assertTrue(completedStatus.contains("has been approved"));
|
||||
return true;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class StringMatchesUUIDPattern extends TypeSafeMatcher<String> {
|
||||
private static final String UUID_REGEX = "[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}";
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(String s) {
|
||||
return s.matches(UUID_REGEX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("a string matching the pattern of a UUID");
|
||||
}
|
||||
|
||||
public static Matcher<String> matchesThePatternOfAUUID() {
|
||||
return new StringMatchesUUIDPattern();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
public class TestExternalEventsApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
SpringApplication.from(ExternalEventsApplication::main)
|
||||
.with(DaprTestContainersConfig.class)
|
||||
.run(args);
|
||||
org.testcontainers.Testcontainers.exposeHostPorts(8080);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
spring.application.name=external-system-interactions
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<artifactId>workflow-app</artifactId>
|
||||
<name>workflow-app</name>
|
||||
<description>Workflow App Example</description>
|
||||
|
||||
<properties>
|
||||
<dapr.spring.version>0.15.0-rc-7</dapr.spring.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dapr.spring</groupId>
|
||||
<artifactId>dapr-spring-boot-starter</artifactId>
|
||||
<version>${dapr.spring.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dapr.spring</groupId>
|
||||
<artifactId>dapr-spring-boot-starter-test</artifactId>
|
||||
<version>${dapr.spring.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
public class WorkflowAppApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(WorkflowAppApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class WorkflowAppConfiguration {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplateBuilder().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ObjectMapper mapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
|
||||
import io.dapr.spring.workflows.config.EnableDaprWorkflows;
|
||||
import io.dapr.springboot.examples.workflowapp.OrderStatus;
|
||||
import io.dapr.springboot.examples.workflowapp.OrderWorkflow;
|
||||
import io.dapr.springboot.examples.workflowapp.Order;
|
||||
import io.dapr.workflows.client.DaprWorkflowClient;
|
||||
import io.dapr.workflows.client.WorkflowInstanceStatus;
|
||||
import io.dapr.workflows.client.WorkflowRuntimeStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@RestController
|
||||
/*
|
||||
* Dapr Workflows and Activities need to be registered in the DI container otherwise
|
||||
* the Dapr runtime does not know this application contains workflows and activities.
|
||||
* In Spring Boot Applications, the `@EnableDaprWorkflow` annotation takes care of registering
|
||||
* all workflows and activities components found in the classpath.
|
||||
*/
|
||||
@EnableDaprWorkflows
|
||||
public class WorkflowAppRestController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(WorkflowAppRestController.class);
|
||||
|
||||
@Autowired
|
||||
private DaprWorkflowClient daprWorkflowClient;
|
||||
|
||||
private String instanceId;
|
||||
|
||||
/**
|
||||
* The DaprWorkflowClient is the API to manage workflows.
|
||||
* Here it is used to schedule a new workflow instance.
|
||||
*
|
||||
* @return the instanceId of the ExternalEventsWorkflow execution
|
||||
*/
|
||||
@PostMapping("start")
|
||||
public String basic(@RequestBody Order order) throws TimeoutException {
|
||||
logger.info("Received order: {}", order);
|
||||
instanceId = daprWorkflowClient.scheduleNewWorkflow(OrderWorkflow.class, order);
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain the status of the workflow
|
||||
*
|
||||
* @return the status of the ExternalEventsWorkflow instance
|
||||
*/
|
||||
@GetMapping("status")
|
||||
public String status() throws TimeoutException {
|
||||
if(instanceId != null && !instanceId.isEmpty()) {
|
||||
WorkflowInstanceStatus instanceState = daprWorkflowClient.getInstanceState(instanceId, true);
|
||||
if (instanceState != null) {
|
||||
if (instanceState.getRuntimeStatus().equals(WorkflowRuntimeStatus.COMPLETED)) {
|
||||
var output = instanceState.readOutputAs(String.class);
|
||||
if (output != null && !output.isEmpty()) {
|
||||
return "Workflow Instance (" + instanceId + ") Status: " + instanceState.getRuntimeStatus().name() + "\n"
|
||||
+ "Output: " + output;
|
||||
}
|
||||
}
|
||||
return "Workflow Instance (" + instanceId + ") Status: " + instanceState.getRuntimeStatus().name();
|
||||
}
|
||||
}
|
||||
return "N/A";
|
||||
}
|
||||
|
||||
/**
|
||||
* Raise a workflow event
|
||||
*/
|
||||
@PostMapping("event")
|
||||
public void raiseEvent(@RequestBody OrderStatus approvalStatus) {
|
||||
daprWorkflowClient.raiseEvent(instanceId, "approval-event", approvalStatus);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
public record ActivityResult(boolean isSuccess, String message) { }
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
public record CustomerInfo(String id, String country) {}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
public record Order(String id, OrderItem orderItem, CustomerInfo customerInfo){}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
public record OrderItem(String productId, String productName, int quantity, BigDecimal totalPrice) {}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
public record OrderStatus(boolean isSuccess, String message){}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
import io.dapr.durabletask.Task;
|
||||
import io.dapr.durabletask.TaskCanceledException;
|
||||
import io.dapr.springboot.examples.workflowapp.activities.*;
|
||||
import io.dapr.workflows.Workflow;
|
||||
import io.dapr.workflows.WorkflowStub;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
public class OrderWorkflow implements Workflow {
|
||||
@Override
|
||||
public WorkflowStub create() {
|
||||
return ctx -> {
|
||||
|
||||
var order = ctx.getInput(Order.class);
|
||||
|
||||
// First two independent activities are called in parallel (fan-out/fan-in pattern):
|
||||
var inventoryTask = ctx.callActivity(CheckInventoryActivity.class.getName(), order.orderItem(), ActivityResult.class);
|
||||
|
||||
var shippingDestinationTask = ctx.callActivity(
|
||||
CheckShippingDestinationActivity.class.getName(), order, ActivityResult.class);
|
||||
|
||||
List<Task<ActivityResult>> tasks = List.of(inventoryTask, shippingDestinationTask);
|
||||
|
||||
var tasksResult = ctx.allOf(tasks).await();
|
||||
|
||||
if( tasksResult.stream().anyMatch(r -> !r.isSuccess())){
|
||||
var message = "Order processing failed. Reason: " + tasksResult.get(0).message();
|
||||
ctx.complete(new OrderStatus(false, message);
|
||||
}
|
||||
|
||||
|
||||
// Two activities are called in sequence (chaining pattern) where the UpdateInventory
|
||||
// activity is dependent on the result of the ProcessPayment activity:
|
||||
var paymentResult = ctx.callActivity(ProcessPaymentActivity.class.getName(), order.orderItem(), ActivityResult.class).await();
|
||||
|
||||
if(paymentResult.isSuccess()){
|
||||
var inventoryResult = ctx.callActivity(ProcessPaymentActivity.class.getName(), order.orderItem(), UpdateInventoryResult.class).await();
|
||||
}
|
||||
|
||||
ShipmentRegistrationStatus shipmentRegistrationStatus = null;
|
||||
try
|
||||
{
|
||||
// The RegisterShipment activity is using pub/sub messaging to communicate with the ShippingApp.
|
||||
ctx.callActivity(RegisterShipmentActivity.class.getName(), order.orderItem(), RegisterShipmentResult.class).await();
|
||||
|
||||
// The ShippingApp will also use pub/sub messaging back to the WorkflowApp and raise an event.
|
||||
// The workflow will wait for the event to be received or until the timeout occurs.
|
||||
shipmentRegistrationStatus = ctx.waitForExternalEvent("", Duration.ofSeconds(300), ShipmentRegistrationStatus.class).await();
|
||||
} catch (TaskCanceledException tce){
|
||||
// Timeout occurred, the shipment-registered-event was not received.
|
||||
var message = "ShipmentRegistrationStatus for " + order.id() + " timed out.";
|
||||
ctx.complete(new OrderStatus(false, message));
|
||||
}
|
||||
|
||||
assert shipmentRegistrationStatus != null;
|
||||
|
||||
if (!shipmentRegistrationStatus.isSuccess())
|
||||
{
|
||||
// This is the compensation step in case the shipment registration event was not successful.
|
||||
ctx.callActivity(ReimburseCustomerActivity.class.getName(), order);
|
||||
var message = "ShipmentRegistrationStatus for {order.Id} failed. Customer is reimbursed.";
|
||||
ctx.complete(new OrderStatus(false, message));
|
||||
}
|
||||
|
||||
ctx.complete(new OrderStatus(true, "Order " + order.id() + " processed successfully."));
|
||||
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
public record ProductInventory(String productId, int quantity) { }
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
public record RegisterShipmentResult() {
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
public record ShipmentRegistrationStatus(String orderId, boolean isSuccess, String message) { }
|
||||
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
package io.dapr.springboot.examples.workflowapp;
|
||||
|
||||
public record UpdateInventoryResult(boolean isSuccess, String message) { }
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.workflowapp.activities;
|
||||
|
||||
import io.dapr.client.DaprClient;
|
||||
import io.dapr.client.domain.State;
|
||||
import io.dapr.springboot.examples.workflowapp.ActivityResult;
|
||||
import io.dapr.springboot.examples.workflowapp.Order;
|
||||
import io.dapr.springboot.examples.workflowapp.OrderItem;
|
||||
import io.dapr.springboot.examples.workflowapp.ProductInventory;
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class CheckInventoryActivity implements WorkflowActivity {
|
||||
|
||||
@Autowired
|
||||
private DaprClient daprClient;
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(CheckInventoryActivity.class);
|
||||
var orderItem = ctx.getInput(OrderItem.class);
|
||||
logger.info("{} : Received input: {}", ctx.getName(), orderItem);
|
||||
|
||||
var productInventory = daprClient.getState("inventory", orderItem.productId(), ProductInventory.class).block();
|
||||
|
||||
if (productInventory == null)
|
||||
{
|
||||
return new ActivityResult(false, "");
|
||||
}
|
||||
|
||||
var isAvailable = productInventory.getValue().quantity() >= orderItem.quantity();
|
||||
return new ActivityResult(isAvailable, "");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.workflowapp.activities;
|
||||
|
||||
import io.dapr.springboot.examples.workflowapp.Order;
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
@Component
|
||||
public class CheckShippingDestinationActivity implements WorkflowActivity {
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(CheckShippingDestinationActivity.class);
|
||||
var order = ctx.getInput(Order.class);
|
||||
// Imagine the order being processed by another system
|
||||
logger.info("{} : Processed Order: {}", ctx.getName(), order.id());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.workflowapp.activities;
|
||||
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
*/
|
||||
@Component
|
||||
public class ProcessPaymentActivity implements WorkflowActivity {
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(ProcessPaymentActivity.class);
|
||||
var message = ctx.getInput(String.class);
|
||||
|
||||
// Imagine a notification being sent to the user
|
||||
logger.info("{} : Sending Notification: {}", ctx.getName(), message);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.workflowapp.activities;
|
||||
|
||||
import io.dapr.springboot.examples.workflowapp.Order;
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class RegisterShipmentActivity implements WorkflowActivity {
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(RegisterShipmentActivity.class);
|
||||
var order = ctx.getInput(Order.class);
|
||||
logger.info("{} : Request Approval for Order: {}", ctx.getName(), order.id());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.workflowapp.activities;
|
||||
|
||||
import io.dapr.springboot.examples.workflowapp.Order;
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ReimburseCustomerActivity implements WorkflowActivity {
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(ReimburseCustomerActivity.class);
|
||||
var order = ctx.getInput(Order.class);
|
||||
logger.info("{} : Request Approval for Order: {}", ctx.getName(), order.id());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
spring.application.name=workflow-app
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import io.dapr.testcontainers.Component;
|
||||
import io.dapr.testcontainers.DaprContainer;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
public class DaprTestContainersConfig {
|
||||
|
||||
@Bean
|
||||
@ServiceConnection
|
||||
public DaprContainer daprContainer() {
|
||||
return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
|
||||
.withAppName("external-system-interactions")
|
||||
.withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true))))
|
||||
.withAppPort(8080)
|
||||
.withAppHealthCheckPath("/actuator/health")
|
||||
.withAppChannelAddress("host.testcontainers.internal");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
public class TestWorkflowAppApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
SpringApplication.from(WorkflowAppApplication::main)
|
||||
.with(DaprTestContainersConfig.class)
|
||||
.run(args);
|
||||
org.testcontainers.Testcontainers.exposeHostPorts(8080);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import io.dapr.client.DaprClient;
|
||||
import io.dapr.springboot.DaprAutoConfiguration;
|
||||
import io.dapr.springboot.examples.workflowapp.OrderStatus;
|
||||
import io.dapr.springboot.examples.workflowapp.Order;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.hamcrest.Description;
|
||||
import org.hamcrest.Matcher;
|
||||
import org.hamcrest.TypeSafeMatcher;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static io.dapr.springboot.examples.StringMatchesUUIDPattern.matchesThePatternOfAUUID;
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SpringBootTest(classes = {TestWorkflowAppApplication.class, DaprTestContainersConfig.class,
|
||||
DaprAutoConfiguration.class, },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
class WorkflowAppTests {
|
||||
|
||||
@Autowired
|
||||
private DaprClient daprClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RestAssured.baseURI = "http://localhost:" + 8080;
|
||||
org.testcontainers.Testcontainers.exposeHostPorts(8080);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testExternalEventsWorkflow() throws InterruptedException {
|
||||
|
||||
var order = new Order("123", "Rubber ducks", 100, 500);
|
||||
given().contentType(ContentType.JSON)
|
||||
.body(order)
|
||||
.when()
|
||||
.post("/start")
|
||||
.then()
|
||||
.statusCode(200).body(matchesThePatternOfAUUID());
|
||||
|
||||
|
||||
String status = given().contentType(ContentType.JSON)
|
||||
.when()
|
||||
.get("/status")
|
||||
.then()
|
||||
.statusCode(200).extract().asString();
|
||||
|
||||
assertTrue(status.contains("RUNNING"));
|
||||
|
||||
OrderStatus approvalStatus = new OrderStatus("123", true);
|
||||
given().contentType(ContentType.JSON)
|
||||
.body(approvalStatus)
|
||||
.when()
|
||||
.post("/event")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
|
||||
// Wait for the workflow instance to complete
|
||||
|
||||
await().atMost(Duration.ofSeconds(2))
|
||||
.pollDelay(500, TimeUnit.MILLISECONDS)
|
||||
.pollInterval(500, TimeUnit.MILLISECONDS)
|
||||
.until(() -> {
|
||||
var completedStatus = given().contentType(ContentType.JSON)
|
||||
.when()
|
||||
.get("/status")
|
||||
.then()
|
||||
.statusCode(200).extract().asString();
|
||||
assertTrue(completedStatus.contains("COMPLETED"));
|
||||
assertTrue(completedStatus.contains("has been approved"));
|
||||
return true;
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
class StringMatchesUUIDPattern extends TypeSafeMatcher<String> {
|
||||
private static final String UUID_REGEX = "[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}";
|
||||
|
||||
@Override
|
||||
protected boolean matchesSafely(String s) {
|
||||
return s.matches(UUID_REGEX);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void describeTo(Description description) {
|
||||
description.appendText("a string matching the pattern of a UUID");
|
||||
}
|
||||
|
||||
public static Matcher<String> matchesThePatternOfAUUID() {
|
||||
return new StringMatchesUUIDPattern();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
spring.application.name=workflow-app
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# Resiliency & Compensation
|
||||
|
||||
This tutorial demonstrates how to improve resiliency when activities are executed and how to include compensation actions when activities return an error. For more on workflow resiliency read the [Dapr docs](https://docs.dapr.io/developing-applications/building-blocks/workflow/workflow-features-concepts/#retry-policies).
|
||||
|
||||
## Inspect the code
|
||||
|
||||
Open the `ResiliencyAndCompensationWorkflow.cs` file in the `tutorials/workflow/csharp/resiliency-and-compensation/ResiliencyAndCompensation` folder. This file contains the definition for the workflow. This workflow implements an activity retry policy on all the associated activities and compensating logic if an activity throws an exception.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
SW((Start
|
||||
Workflow))
|
||||
A1[MinusOne]
|
||||
A2[Division]
|
||||
EX{Division
|
||||
Exception?}
|
||||
A3[PlusOne]
|
||||
EW((End
|
||||
Workflow))
|
||||
SW --> A1
|
||||
A1 --> A2
|
||||
A2 --> EX
|
||||
EX -- yes --> A3
|
||||
EX -- no --> EW
|
||||
A3 --> EW
|
||||
```
|
||||
|
||||
## Run the tutorial
|
||||
|
||||
1. Use a terminal to navigate to the `tutorials/workflow/csharp/resiliency-and-compensation` folder.
|
||||
2. Build the project using the .NET CLI.
|
||||
|
||||
```bash
|
||||
dotnet build ./ResiliencyAndCompensation/
|
||||
```
|
||||
|
||||
3. Use the Dapr CLI to run the Dapr Multi-App run file
|
||||
|
||||
<!-- STEP
|
||||
name: Run multi app run template
|
||||
expected_stdout_lines:
|
||||
- 'Started Dapr with app id "resiliency"'
|
||||
expected_stderr_lines:
|
||||
working_dir: .
|
||||
output_match_mode: substring
|
||||
background: true
|
||||
sleep: 15
|
||||
timeout_seconds: 30
|
||||
-->
|
||||
```bash
|
||||
dapr run -f .
|
||||
```
|
||||
<!-- END_STEP -->
|
||||
|
||||
4. Use the POST request in the [`resiliency-compensation.http`](./resiliency-compensation.http) file to start the workflow with a workflow input value of `1`, or use this cURL command:
|
||||
|
||||
```bash
|
||||
curl -i --request POST \
|
||||
--url http://localhost:8080/start/1
|
||||
```
|
||||
|
||||
When the workflow input is `1`, the `MinusOne` activity will subtract `1` resulting in a `0`. This value is passed to the `Division` activity, which will throw an error because the divisor is `0`. The `Division` activity will be retried three times but all will fail the same way as the divisor has not changed. Finally the compensation action `PlusOne` will be executed, increasing the value back to `1` before returning the result.
|
||||
|
||||
The app logs should output the following:
|
||||
|
||||
```txt
|
||||
== APP - resiliency == MinusOne: Received input: 1.
|
||||
== APP - resiliency == Division: Received divisor: 0.
|
||||
== APP - resiliency == Division: Received divisor: 0.
|
||||
== APP - resiliency == Division: Received divisor: 0.
|
||||
== APP - resiliency == PlusOne: Received input: 0.
|
||||
```
|
||||
|
||||
5. Use the GET request in the [`resiliency-compensation.http`](./resiliency-compensation.http) file to get the status of the workflow, or use this cURL command:
|
||||
|
||||
```bash
|
||||
curl --request GET --url http://localhost:3564/v1.0/workflows/dapr/<INSTANCEID>
|
||||
```
|
||||
|
||||
Where `<INSTANCEID>` is the workflow instance ID you received in the `Location` header in the previous step.
|
||||
```
|
||||
|
||||
Since `1` is used as the input, the expected serialized output of the workflow is:
|
||||
|
||||
```txt
|
||||
"1"
|
||||
```
|
||||
|
||||
The expected serialized custom status field of the workflow output is:
|
||||
|
||||
```txt
|
||||
"\"Compensated MinusOne activity with PlusOne activity.\""
|
||||
```
|
||||
|
||||
6. Stop the Dapr Multi-App run process by pressing `Ctrl+C`.
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.5</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<artifactId>resiliency-and-compensation</artifactId>
|
||||
<name>resiliency-and-compensation</name>
|
||||
<description>Resiliency and Compensation Workflow Example</description>
|
||||
|
||||
<properties>
|
||||
<dapr.spring.version>0.15.0-rc-7</dapr.spring.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dapr.spring</groupId>
|
||||
<artifactId>dapr-spring-boot-starter</artifactId>
|
||||
<version>${dapr.spring.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.dapr.spring</groupId>
|
||||
<artifactId>dapr-spring-boot-starter-test</artifactId>
|
||||
<version>${dapr.spring.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
@apphost=http://localhost:8080
|
||||
|
||||
### Start the Resiliency and Compensation Workflow workflow
|
||||
# @name startWorkflowRequest
|
||||
@input=1
|
||||
POST {{ apphost }}/start/{{input}}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
public class ResiliencyAndCompensationApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ResiliencyAndCompensationApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
@Configuration
|
||||
public class ResiliencyAndCompensationConfiguration {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplateBuilder().build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ObjectMapper mapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
|
||||
import io.dapr.spring.workflows.config.EnableDaprWorkflows;
|
||||
import io.dapr.springboot.examples.resiliency.ResiliencyAndCompensationWorkflow;
|
||||
import io.dapr.workflows.client.DaprWorkflowClient;
|
||||
import io.dapr.workflows.client.WorkflowInstanceStatus;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
@RestController
|
||||
@EnableDaprWorkflows
|
||||
public class ResiliencyAndCompensationRestController {
|
||||
|
||||
private final Logger logger = LoggerFactory.getLogger(ResiliencyAndCompensationRestController.class);
|
||||
|
||||
@Autowired
|
||||
private DaprWorkflowClient daprWorkflowClient;
|
||||
|
||||
|
||||
/**
|
||||
* Run Resiliency And Compensation Workflow
|
||||
*
|
||||
* @return the instanceId of the ResiliencyAndCompensationWorkflow execution
|
||||
*/
|
||||
@PostMapping("start/{input}")
|
||||
public String start(@PathVariable("input") Integer input) throws TimeoutException {
|
||||
return daprWorkflowClient.scheduleNewWorkflow(ResiliencyAndCompensationWorkflow.class, input);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.resiliency;
|
||||
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
*/
|
||||
@Component
|
||||
public class DivisionActivity implements WorkflowActivity {
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(DivisionActivity.class);
|
||||
var divisor = ctx.getInput(Integer.class);
|
||||
|
||||
logger.info("{} : Received divisor: {}", ctx.getName(), divisor);
|
||||
|
||||
var result = Math.round(100d / divisor);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.resiliency;
|
||||
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
*/
|
||||
@Component
|
||||
public class MinusOneActivity implements WorkflowActivity {
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(MinusOneActivity.class);
|
||||
var input = ctx.getInput(Integer.class);
|
||||
|
||||
logger.info("{}: Received input: {}", ctx.getName(), input);
|
||||
var result = input - 1;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.resiliency;
|
||||
|
||||
import io.dapr.workflows.WorkflowActivity;
|
||||
import io.dapr.workflows.WorkflowActivityContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/*
|
||||
*/
|
||||
@Component
|
||||
public class PlusOneActivity implements WorkflowActivity {
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
||||
@Override
|
||||
public Object run(WorkflowActivityContext ctx) {
|
||||
Logger logger = LoggerFactory.getLogger(PlusOneActivity.class);
|
||||
var input = ctx.getInput(Integer.class);
|
||||
|
||||
logger.info("{}: Received input: {}", ctx.getName(), input);
|
||||
var result = input + 1;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2023 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples.resiliency;
|
||||
|
||||
import io.dapr.durabletask.CompositeTaskFailedException;
|
||||
import io.dapr.durabletask.TaskFailedException;
|
||||
import io.dapr.workflows.Workflow;
|
||||
import io.dapr.workflows.WorkflowStub;
|
||||
import io.dapr.workflows.WorkflowTaskOptions;
|
||||
import io.dapr.workflows.WorkflowTaskRetryPolicy;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Component
|
||||
public class ResiliencyAndCompensationWorkflow implements Workflow {
|
||||
@Override
|
||||
public WorkflowStub create() {
|
||||
return ctx -> {
|
||||
|
||||
|
||||
var counter = ctx.getInput(Integer.class);
|
||||
|
||||
WorkflowTaskRetryPolicy workflowTaskRetryPolicy = new WorkflowTaskRetryPolicy(3,
|
||||
Duration.ofSeconds(2),
|
||||
1.0,
|
||||
Duration.ofSeconds(10),
|
||||
Duration.ofSeconds(15));
|
||||
|
||||
WorkflowTaskOptions defaultActivityRetryOptions = new WorkflowTaskOptions(workflowTaskRetryPolicy);
|
||||
|
||||
Integer result1 = ctx.callActivity(MinusOneActivity.class.getName(), counter, defaultActivityRetryOptions, Integer.class).await();
|
||||
|
||||
Integer workflowResult;
|
||||
try {
|
||||
workflowResult = ctx.callActivity(DivisionActivity.class.getName(), result1, defaultActivityRetryOptions, Integer.class).await();
|
||||
} catch (TaskFailedException wtfe) {
|
||||
System.out.println("WTFE" + wtfe);
|
||||
wtfe.printStackTrace();
|
||||
|
||||
//Check Exception
|
||||
|
||||
workflowResult = ctx.callActivity(PlusOneActivity.class.getName(), result1, defaultActivityRetryOptions, Integer.class).await();
|
||||
|
||||
//@TODO: not supported yet
|
||||
//ctx.setCustomStatus()
|
||||
|
||||
}
|
||||
|
||||
|
||||
ctx.complete(workflowResult);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
spring.application.name=workflow-management
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import io.dapr.testcontainers.Component;
|
||||
import io.dapr.testcontainers.DaprContainer;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static io.dapr.testcontainers.DaprContainerConstants.DAPR_RUNTIME_IMAGE_TAG;
|
||||
|
||||
@TestConfiguration(proxyBeanMethods = false)
|
||||
public class DaprTestContainersConfig {
|
||||
|
||||
@Bean
|
||||
@ServiceConnection
|
||||
public DaprContainer daprContainer() {
|
||||
return new DaprContainer(DAPR_RUNTIME_IMAGE_TAG)
|
||||
.withAppName("workflow-patterns-app")
|
||||
.withComponent(new Component("kvstore", "state.in-memory", "v1", Collections.singletonMap("actorStateStore", String.valueOf(true))))
|
||||
.withAppPort(8080)
|
||||
.withAppHealthCheckPath("/actuator/health")
|
||||
.withAppChannelAddress("host.testcontainers.internal");
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
|
||||
@SpringBootApplication
|
||||
public class TestWorkflowManagementApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
SpringApplication.from(ResiliencyAndCompensationApplication::main)
|
||||
.with(DaprTestContainersConfig.class)
|
||||
.run(args);
|
||||
org.testcontainers.Testcontainers.exposeHostPorts(8080);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* Copyright 2025 The Dapr Authors
|
||||
* Licensed 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 io.dapr.springboot.examples;
|
||||
|
||||
import io.dapr.client.DaprClient;
|
||||
import io.dapr.springboot.DaprAutoConfiguration;
|
||||
import io.restassured.RestAssured;
|
||||
import io.restassured.http.ContentType;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static io.restassured.RestAssured.given;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest(classes = {TestWorkflowManagementApplication.class, DaprTestContainersConfig.class,
|
||||
DaprAutoConfiguration.class, },
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
class WorkflowManagementAppTests {
|
||||
|
||||
@Autowired
|
||||
private DaprClient daprClient;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
RestAssured.baseURI = "http://localhost:" + 8080;
|
||||
org.testcontainers.Testcontainers.exposeHostPorts(8080);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testWorkflowManagement() {
|
||||
String workflowId = given().contentType(ContentType.JSON)
|
||||
.pathParam("counter", 0)
|
||||
.when()
|
||||
.post("/start/{counter}")
|
||||
.then()
|
||||
.statusCode(200)
|
||||
.extract().asString();
|
||||
|
||||
assertNotNull(workflowId);
|
||||
|
||||
|
||||
String status = given().contentType(ContentType.JSON)
|
||||
.pathParam("instanceId", workflowId)
|
||||
.when()
|
||||
.get("/status/{instanceId}")
|
||||
.then()
|
||||
.statusCode(200).extract().asString();
|
||||
|
||||
assertTrue(status.contains("RUNNING"));
|
||||
|
||||
|
||||
given().contentType(ContentType.JSON)
|
||||
.pathParam("instanceId", workflowId)
|
||||
.when()
|
||||
.post("/suspend/{instanceId}")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
|
||||
|
||||
status = given().contentType(ContentType.JSON)
|
||||
.pathParam("instanceId", workflowId)
|
||||
.when()
|
||||
.get("/status/{instanceId}")
|
||||
.then()
|
||||
.statusCode(200).extract().asString();
|
||||
|
||||
assertTrue(status.contains("SUSPENDED"));
|
||||
|
||||
given().contentType(ContentType.JSON)
|
||||
.pathParam("instanceId", workflowId)
|
||||
.when()
|
||||
.post("/resume/{instanceId}")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
|
||||
|
||||
status = given().contentType(ContentType.JSON)
|
||||
.pathParam("instanceId", workflowId)
|
||||
.when()
|
||||
.get("/status/{instanceId}")
|
||||
.then()
|
||||
.statusCode(200).extract().asString();
|
||||
|
||||
assertTrue(status.contains("RUNNING"));
|
||||
|
||||
|
||||
given().contentType(ContentType.JSON)
|
||||
.pathParam("instanceId", workflowId)
|
||||
.when()
|
||||
.post("/terminate/{instanceId}")
|
||||
.then()
|
||||
.statusCode(200);
|
||||
|
||||
status = given().contentType(ContentType.JSON)
|
||||
.pathParam("instanceId", workflowId)
|
||||
.when()
|
||||
.get("/status/{instanceId}")
|
||||
.then()
|
||||
.statusCode(200).extract().asString();
|
||||
|
||||
assertTrue(status.contains("TERMINATED"));
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
spring.application.name=workflow-management
|
||||
Loading…
Reference in New Issue