Coder Social home page Coder Social logo

diagrid-labs / dapr-workflow-demos Goto Github PK

View Code? Open in Web Editor NEW
50.0 4.0 6.0 416 KB

Demos of the Dapr Workflow building block API: chaining, fan-out/fan-in, monitor, external system interaction, and a complete checkout service.

License: Apache License 2.0

C# 99.71% Shell 0.29%
dapr distributed-systems dotnet workflow workflow-engine

dapr-workflow-demos's Introduction

Dapr workflow demos

Demos applications that use the Dapr Workflow building block.

Read the blog posts:

Prerequisites

  1. .NET 8 SDK

  2. Dapr CLI

    • Ensure that you're using v1.11 (or higher) of the Dapr runtime and the CLI, since there have been breaking changes to the Workflow API from v1.10 to v1.11.
  3. A REST client, such as cURL, or the VSCode REST client extension.

    The VSCode REST client is configured as a recommended extension when opening this repo in VSCode.

Hello world workflow sample

The Hello World Workflow sample is a very basic workflow with just one activity that returns a random greeting. The workflow takes a name as input and returns a greeting with the name as output.

graph TB
    A[Start]
    B[CreateGreetingActivity]
    C[End]
    A -->|"input: {name}"| B -->|"output: {greeting} {name}"| C

Run the HelloWorldWorkflowSample app

  1. Change to the BasicWorkflowSamples directory and build the ASP.NET app:

    cd BasicWorkflowSamples
    dotnet build
  2. Run the app using the Dapr CLI:

    dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run

    Ensure the --app-port is the same as the port specified in the launchSettings.json file.

  3. Start the HelloWorldWorkflow via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:

    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/HelloWorldWorkflow/start?instanceID=1234a \
      -H "Content-Type: application/text/plain" \
      -d '"World"'

    Note that 1234a in the URL is the workflow instance ID. This can be any string you want.

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>"
    }
  4. Check the workflow status via Workflow HTTP API:

    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234a

    Expected result:

    {
    "instanceID": "<WORKFLOW_ID>",
    "workflowName": "HelloWorldWorkflow",
    "createdAt": "2023-06-19T13:19:18.316956600Z",
    "lastUpdatedAt": "2023-06-19T13:19:18.333226200Z",
    "runtimeStatus": "COMPLETED",
    "properties": {
        "dapr.workflow.custom_status": "",
        "dapr.workflow.input": "\"World\"",
        "dapr.workflow.output": "\"Ciao World\""
    }

} ```

Chaining workflow sample

The Chaining Workflow sample is a workflow that chains three activities of the same type CreateGreetingActivity, each prepending a random greeting to the input. The output of the activity is used as an input for the next activity in the chain.

graph TB
    A[Start]
    B1[CreateGreetingActivity]
    B2[CreateGreetingActivity]
    B3[CreateGreetingActivity]
    C[End]
    A -->|"input: {name}"| B1 -->|"output/input: {greeting1} {name}"| B2
    B2 -->|"output/input: {greeting2} {greeting1} {name}"| B3
    B3 -->|"output: {greeting3} {greeting2} {greeting1} {name}"| C
  1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:

    cd BasicWorkflowSamples
    dotnet build
    dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
  2. Start the ChainingWorkflow via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:

    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/ChainingWorkflow/start?instanceID=1234b \
      -H "Content-Type: application/text/plain" \
      -d '"World"'

    Note that 1234b in the URL is the workflow instance ID. This can be any string you want.

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>"
    }
  3. Check the workflow status via Workflow HTTP API:

    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234b

    Expected result:

    {
    "instanceID": "<WORKFLOW_ID>",
    "workflowName": "ChainingWorkflow",
    "createdAt": "2023-06-19T13:21:08.611149200Z",
    "lastUpdatedAt": "2023-06-19T13:21:08.647482Z",
    "runtimeStatus": "COMPLETED",
    "properties": {
        "dapr.workflow.custom_status": "",
        "dapr.workflow.input": "\"World\"",
        "dapr.workflow.output": "\"Hello Hi Konnichiwa World\""
        }
    }

Fan-out / Fan-in workflow sample

The Fan-out / Fan-in Workflow sample is a workflow that fans out and schedules an activity (CreateGreetingActivity) for each element in the input array and waits until all of the activities are completed. The output of the workflow is an array of greetings.

graph TB
    A[Start]
    A1([for each name in names])
    B1[CreateGreetingActivity]
    B2[CreateGreetingActivity]
    B3[CreateGreetingActivity]
    C([Task.WhenAll])
    D[End]
    A --> |"input: [{name1}, {name2}, {name3}]"| A1
    A1 -->|"input: {name1}"| B1 -->|"output: {greeting1} {name1}"| C
    A1 -->|"input: {name2}"| B2 -->|"output: {greeting2} {name2}"| C
    A1 -->|"input: {name3}"| B3 -->|"output: {greeting3} {name3}"| C
    C -->|"output: [{greeting1} {name1}, {greeting2} {name2}, {greeting3} {name3}]"| D
  1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:

    cd BasicWorkflowSamples
    dotnet build
    dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
  2. Start the FanOutFanInWorkflow via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:

    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/FanOutFanInWorkflow/start?instanceID=1234c \
      -H "Content-Type: application/json" \
      -d '["Amsterdam", "Chicago", "New York"]'

    Note that 1234c in the URL is the workflow instance ID. This can be any string you want.

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>"
    }
  3. Check the workflow status via Workflow HTTP API:

    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234c

    Expected result:

    {
    "instanceID": "<WORKFLOW_ID>",
    "workflowName": "FanOutFanInWorkflow",
    "createdAt": "2023-06-19T13:22:42.056622400Z",
    "lastUpdatedAt": "2023-06-19T13:22:42.093666600Z",
    "runtimeStatus": "COMPLETED",
    "properties": {
        "dapr.workflow.custom_status": "",
        "dapr.workflow.input": "[\"Amsterdam\",\"Chicago\",\"New York\"]",
        "dapr.workflow.output": "[\"Hi Amsterdam\",\"Hola Chicago\",\"Guten Tag New York\"]"
        }
    }

Monitor workflow sample

The Monitor sample is a workflow with a numeric input (counter) and restarts the workflow (with an updated counter) until the counter reaches 10. The workflow diagram is as follows:

graph TB
    A[Start]
    B[CreateGreetingActivity]
    C{counter < 10}
    D[End]
    A --> |"input: {counter}"| B
    B -->|"output: {greeting} {counter}"| C
    C --> |"[Yes] {counter+=1}"| A
    C -->|"[No] output: {greeting} 10"| D
  1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:

    cd BasicWorkflowSamples
    dotnet build
    dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
  2. Start the MonitorWorkflow via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:

    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/MonitorWorkflow/start?instanceID=1234d \
      -H "Content-Type: application/text/plain" \
      -d '0'

    Note that 1234d in the URL is the workflow instance ID. This can be any string you want.

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>"
    }
  3. Check the workflow status via Workflow HTTP API:

    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234d

    Expected result:

    {
    "instanceID": "<WORKFLOW_ID>",
    "workflowName": "MonitorWorkflow",
    "createdAt": "2023-06-19T13:24:23.004744700Z",
    "lastUpdatedAt": "2023-06-19T13:24:23.016307900Z",
    "runtimeStatus": "COMPLETED",
    "properties": {
        "dapr.workflow.custom_status": "",
        "dapr.workflow.input": "10",
        "dapr.workflow.output": "\"Hey 10\""
        }
    }

Timer workflow sample

The Timer sample is a workflow with a TimerWorkflowInput object that contains a name and a date. If the date from the input is larger that the current date, a timer is started and the workflow will only continue once the timer has completed.:

graph TB
    A[Start]
    B{input date > current date}
    C([CreateTimer])
    D[CreateGreetingActivity]
    E[End]
    A --> |"input: {date, name}"| B
    B -->|"[Yes]"| C
    C -->|"Wait"| A
    B -->|"[No] input: {name}"| D
    D -->|"output: {greeting} {name}"| E

  1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:

    cd BasicWorkflowSamples
    dotnet build
    dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
  2. Start the TimerWorkflow via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:

    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/TimerWorkflow/start?instanceID=1234e \
      -H "Content-Type: application/json" \
      -d '{"DateTime": "2023-05-29T13:44:00+00:00","Name":"World"}'

    Note that 1234e in the URL is the workflow instance ID. This can be any string you want.

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>"
    }
  3. Check the workflow status via Workflow HTTP API:

    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234e

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>",
        "workflowName": "TimerWorkflow",
        "createdAt": "2023-06-19T13:25:59.745344700Z",
        "lastUpdatedAt": "2023-06-19T13:25:59.768925500Z",
        "runtimeStatus": "COMPLETED",
        "properties": {
            "dapr.workflow.custom_status": "",
            "dapr.workflow.input": "{\"DateTime\": \"2023-05-29T13:44:00+00:00\", \"Name\": \"World\"}",
            "dapr.workflow.output": "\"Guten Tag World at 2023-05-29 15:44:00\""
        }
    }

External interaction workflow sample

The External interaction sample is a workflow that will wait with execution of the workflow until an external event has been received or the timeout has expired.

graph TB
    A[Start]
    B([WaitForExternalEventAsync])
    C{IsApproved}
    D[CreateGreetingActivity]
    F[ExternalEvent]
    E[End]
    A --> B
    F --> B
    B --> C
    C -->|"[Yes]"| D
    C -->|"[No / Timeout]"| E
    D --> E
  1. Ensure that the BasicWorkflowSamples app is still running, if not change to the BasicWorkflowSamples directory, build the app, and run the app using the Dapr CLI:

    cd BasicWorkflowSamples
    dotnet build
    dapr run --app-id basic-workflows --app-port 5065 --dapr-http-port 3500 --resources-path ./ResourcesLocal dotnet run
  2. Start the ExternalInteractionWorkflow via the Workflow HTTP API using cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:

    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/ExternalInteractionWorkflow/start?instanceID=1234f \
      -H "Content-Type: application/text/plain" \
      -d '"World"'

    Note that 1234f in the URL is the workflow instance ID. This can be any string you want.

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>"
    }
  3. Check the workflow status via Workflow HTTP API:

    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234f

    If you check the status within the specified timeout, the runtimeStatus will be RUNNING:

    {
        "instanceID": "<WORKFLOW_ID>",
        "workflowName": "ExternalInteractionWorkflow",
        "createdAt": "2023-07-27T11:35:54.446941200Z",
        "lastUpdatedAt": "2023-07-27T11:35:55.694310900Z",
        "runtimeStatus": "RUNNING",
        "properties": {
            "dapr.workflow.custom_status": "",
            "dapr.workflow.input": "\"World\""
        }
    }

    If you wait until the timeout has expired, the runtimeStatus will be COMPLETED, with a custom_status that the wait for external event is cancelled due to a timeout:

    {
        "instanceID": "2283569d-5d56-4041-bd63-7df35fa3c879",
        "workflowName": "ExternalInteractionWorkflow",
        "createdAt": "2023-07-27T14:29:37.084711800Z",
        "lastUpdatedAt": "2023-07-27T14:29:57.011878800Z",
        "runtimeStatus": "COMPLETED",
        "properties": {
            "dapr.workflow.custom_status": "\"Wait for external event is cancelled due to timeout.\"",
            "dapr.workflow.input": "\"World\"",
            "dapr.workflow.output": "\"\""
        }
    }
  4. Now start the workflow again, raise an event within the timeout duration by calling the raiseEvent endpoint, and retrieve the status of the workflow. You can use cURL, or use the basicworkflows.http file if you're using VSCode with the REST client:

     curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/ExternalInteractionWorkflow/start?instanceID=1234f \
      -H "Content-Type: application/text/plain" \
      -d '"World"'
    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/1234f/raiseEvent/ \
     -H "Content-Type: application/json" \
     -d '{"IsApproved":true}'
    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234f

    The runtimeStatus should now be COMPLETED:

    {
        "instanceID": "<WORKFLOW_ID>",
        "workflowName": "ExternalInteractionWorkflow",
        "createdAt": "2023-07-27T11:50:16.526722900Z",
        "lastUpdatedAt": "2023-07-27T11:50:19.172489800Z",
        "runtimeStatus": "COMPLETED",
        "properties": {
            "dapr.workflow.custom_status": "",
            "dapr.workflow.input": "\"World\"",
            "dapr.workflow.output": "\"Hello World\""
        }
    }

CheckoutService sample

flowchart LR
A[CheckoutService]
B[PaymentService]
C[(Inventory DB)]
A --> B
A --> C

The CheckoutService app contains workflow that processes an order. The workflow takes an OrderItem as input and returns a CheckoutResult as output. The CheckoutWorkflow workflow uses these activities:

  • NotifyActivity: Notifies the customer of the progress of the order.
  • CheckInventoryActivity: Checks if the inventory is sufficient.
  • ProcessPaymentActivity: Processes the payment, by calling another Dapr service (PaymentService).
  • UpdateInventoryActivity: Updates the inventory after checking it again.
  • RefundPaymentActivity: Refunds the payment if the UpdateInventoryActivity throws an exception.
graph TD
    A[Start]
    B[NotifyActivity]
    C[CheckInventoryActivity]
    X{Sufficient Inventory?}
    D[ProcessPaymentActivity]
    P{Payment successful?}
    E[UpdateInventoryActivity]
    F[RefundPaymentActivity]
    BB[NotifyActivity]
    BBB[NotifyActivity]
    XX{Sufficient Inventory?}
    Z[End]
    A --> |OrderItem| B --> C
    C --> X
    X -->|"[Yes]"| D
    X -->|"[No] CheckoutResult(Processed:false)"| Z
    D --> P
    P -->|"[Yes]"| E --> XX
    P -->|"[No] CheckoutResult(Processed:false)"| Z
    XX -->|"[Yes]"| BB --> |"CheckoutResult(Processed:true)"| Z
    XX -->|"[No]"| BBB --> F --> |"CheckoutResult(Processed:false)"| Z

The CheckInventoryActivity and UpdateInventoryActivity classes use Dapr's state management building block to manage the inventory in a Redis state store.

The ProcessPaymentActivity and RefundPaymentActivity call out to the PaymentService.

Next to the workflow, this application has an InventoryController with the following endpoints:

  • GET http://localhost:5064/inventory: retrieves the inventory
  • DELETE http://localhost:5064/inventory: clears the inventory
  • POST http://localhost:5064/inventory/restock: restocks the inventory

The InventoryController also uses Dapr's state management building block.

Changing the isPaymentSuccess flag

The CheckoutService app relies on the PaymentService app to process the payment. The PaymentService app is a small ASP.NET app that exposes two endpoints:

  • /pay: processes the payment
  • /refund: refunds the payment

The PaymentService app uses the Dapr Configuration API to read the isPaymentSuccess configuration item from the configuration store (Redis). If the item key is not present, or if the item value is set to the string "true", the payment will be successful. If the item value is set to the string "false", the payment will fail. Use this setting to simulate a failed payment and check the workflow result.

Setting the configuration item is done via the redis-cli in the dapr_redis docker container:

docker exec dapr_redis redis-cli MSET isPaymentSuccess "true"

To configure the PaymentService to return a failed payment response use:

docker exec dapr_redis redis-cli MSET isPaymentSuccess "false"

Set the isPaymentSuccess config item to "true" before continuing.

Build the services

  1. Open a terminal, change to the OrderProcess/PaymentService directory and build the ASP.NET app:

    cd OrderProcess/PaymentService
    dotnet build
  2. Change to the OrderProcess/CheckoutService directory and build the ASP.NET app:

    cd OrderProcess/CheckoutService
    dotnet build

Run the services

  1. Using the terminal, change to the OrderProcess directory and start the services using the Dapr CLI with multi-app run:

    dapr run -f .

    This will start both the CheckoutService and PaymentService using the multi-app run configuration specified in the dapr.yaml file.

  2. Check the inventory using cURL, or use the checkout.http file if you're using VSCode with the REST client:

    curl -X POST http://localhost:5064/inventory/restock

    If the quantity of the items is not 0, clear the inventory by running:

    curl -X POST http://localhost:5064/inventory/clear
  3. Try ordering 100 paperclips while the inventory is not sufficient. Start the CheckoutWorkflow via the Workflow HTTP API:

    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/CheckoutWorkflow/start?instanceID=1234f \
      -H "Content-Type: application/json" \
      -d '{"Name": "Paperclips", "Quantity": 100}'

    Note that 1234f in the URL is the workflow instance ID. This can be any string you want.

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>"
    }

    Pay attention to the console output. A message will appear that indicates the inventory is insufficient.

  4. Check the workflow status via Workflow HTTP API:

    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234f

    Expected result:

    {
    "instanceID": "<WORKFLOW_ID>",
    "workflowName": "CheckoutWorkflow",
    "createdAt": "2023-06-19T13:37:30.261385700Z",
    "lastUpdatedAt": "2023-06-19T13:37:30.303315100Z",
    "runtimeStatus": "COMPLETED",
    "properties": {
        "dapr.workflow.custom_status": "\"Stopped order process due to insufficient inventory.\"",
        "dapr.workflow.input": "{\"Name\": \"Paperclips\", \"Quantity\": 125}",
        "dapr.workflow.output": "{\"Processed\":false}"
        }
    }

    Depending on how quick the status is retrieved after starting the workflow, the dapr.workflow.runtime_status could still be "RUNNING". Repeat the GET status request until the status is "COMPLETED".

  5. Restock the inventory:

    curl -X POST http://localhost:5064/inventory/restock

    Expected result: HTTP 200 OK

  6. Try ordering paperclips again, now within the limits of the inventory. Start the CheckoutWorkflow via the Workflow HTTP API:

    curl -i -X POST http://localhost:3500/v1.0-alpha1/workflows/dapr/CheckoutWorkflow/start?instanceID=1234g \
     -H "Content-Type: application/json" \
     -d '{"Name": "Paperclips", "Quantity": 25}'

    Note that 1234g in the URL is the workflow instance ID. This can be any string you want.

    Expected result:

    {
        "instanceID": "<WORKFLOW_ID>"
    }

    Pay attention to the console output. Messages will appear that indicate the inventory is sufficient and payment has been processed successfully.

  7. Check the workflow status via Workflow HTTP API:

    curl -i -X GET http://localhost:3500/v1.0-alpha1/workflows/dapr/1234g

    Expected result:

    {
    "instanceID": "<WORKFLOW_ID>",
    "workflowName": "CheckoutWorkflow",
    "createdAt": "2023-06-19T13:35:42.126109Z",
    "lastUpdatedAt": "2023-06-19T13:35:53.644632200Z",
    "runtimeStatus": "COMPLETED",
    "properties": {
        "dapr.workflow.custom_status": "",
        "dapr.workflow.input": "{\"Name\": \"Paperclips\",\"Quantity\": 25}",
        "dapr.workflow.output": "{\"Processed\":true}"
        }
    }
  8. Inspect the logs in ZipKin: localhost:9411/zipkin. Find the entry marked checkout:create_orchestration||checkoutworkflow and show the details. You'll now see a timeline of the workflow at the top, and the activities underneath.

    Checkout workflow in Zipkin

Unhappy paths

Now try these different scenarios and check the workflow result.

Start the CheckoutWorkflow with:

  1. Shutting down the CheckoutService app once the CheckoutWorkflow has started. Restart the CheckoutService and watch the logs.
  2. A failing payment (set the isPaymentSuccess configuration item to "false").
  3. Shut down the PaymentService completely (check the logs for the retry attempts).

Resources

  1. Dapr Workflow overview.
  2. How to: Author and manage Dapr Workflow in the .NET SDK

More information

Any questions or comments about this sample? Join the Dapr discord and post a message the #workflow channel. Have you made something with Dapr? Post a message in the #show-and-tell channel, we love to see your creations!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.