mirror of https://github.com/dapr/quickstarts.git
Merge branch 'release-1.13' of github.com:dapr/quickstarts into release-1.13
This commit is contained in:
commit
a35571fc9a
|
@ -18,6 +18,7 @@ release.properties
|
|||
mvnw
|
||||
packages
|
||||
**/__pycache__/
|
||||
**/dist/
|
||||
Debug/
|
||||
|
||||
# IDE generated files and directories
|
||||
|
|
|
@ -36,7 +36,7 @@ internal class SmokeDetectorActor : Actor, ISmartDevice
|
|||
/// </summary>
|
||||
protected override Task OnDeactivateAsync()
|
||||
{
|
||||
// Provides Opportunity to perform optional cleanup.
|
||||
// Provides opportunity to perform optional cleanup.
|
||||
Console.WriteLine($"Deactivating actor id: {Id}");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
@ -47,9 +47,12 @@ internal class SmokeDetectorActor : Actor, ISmartDevice
|
|||
/// <param name="data">the user-defined MyData which will be stored into state store as "device_data" state</param>
|
||||
public async Task<string> SetDataAsync(SmartDeviceData data)
|
||||
{
|
||||
// Data is saved to configured state store *implicitly* after each method execution by Actor's runtime.
|
||||
// Data can also be saved *explicitly* by calling this.StateManager.SaveStateAsync();
|
||||
// State to be saved must be DataContract serializable.
|
||||
// This set state action can happen along other state changing operations in each actor method and those changes will be maintained
|
||||
// in a local cache to be committed as a single transaction to the backing store when the method has completed. As such, there is
|
||||
// no need to (and in fact makes your code less transactional) call `this.StateManager.SaveStateAsync()` as it will be automatically
|
||||
// invoked by the actor runtime following the conclusion of this method as part of the internal `OnPostActorMethodAsyncInternal` method.
|
||||
|
||||
// Note also that all saved state must be DataContract serializable.
|
||||
await StateManager.SetStateAsync<SmartDeviceData>(
|
||||
deviceDataKey,
|
||||
data);
|
||||
|
|
|
@ -1,21 +1,19 @@
|
|||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Dapr.Client;
|
||||
|
||||
var baseURL = (Environment.GetEnvironmentVariable("BASE_URL") ?? "http://localhost") + ":" + (Environment.GetEnvironmentVariable("DAPR_HTTP_PORT") ?? "3500");
|
||||
|
||||
var client = new HttpClient();
|
||||
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
|
||||
// Adding app id as part of the header
|
||||
client.DefaultRequestHeaders.Add("dapr-app-id", "order-processor");
|
||||
var client = DaprClient.CreateInvokeHttpClient(appId: "order-processor");
|
||||
|
||||
for (int i = 1; i <= 20; i++) {
|
||||
var order = new Order(i);
|
||||
var orderJson = JsonSerializer.Serialize<Order>(order);
|
||||
var content = new StringContent(orderJson, Encoding.UTF8, "application/json");
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
Console.CancelKeyPress += (object? sender, ConsoleCancelEventArgs e) => cts.Cancel();
|
||||
|
||||
// Invoking a service
|
||||
var response = await client.PostAsync($"{baseURL}/orders", content);
|
||||
var response = await client.PostAsJsonAsync("/orders", order, cts.Token);
|
||||
|
||||
Console.WriteLine("Order passed: " + order);
|
||||
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
|
|
|
@ -7,4 +7,9 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapr.Client" Version="1.12.0" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -74,8 +74,8 @@ sleep: 15
|
|||
cd ./order-processor
|
||||
dapr run --app-id order-processor --resources-path ../../../resources/ -- dotnet run
|
||||
```
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Stop and clean up application processes
|
||||
|
||||
dapr stop --app-id order-processor
|
||||
<!-- END_STEP -->
|
|
@ -87,10 +87,8 @@ You're up and running! Both Dapr and your app logs will appear here.
|
|||
== APP == Getting Order: Order { orderId = 3 }
|
||||
== APP == Deleting Order: Order { orderId = 3 }
|
||||
```
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Stop and clean up application processes
|
||||
|
||||
```bash
|
||||
dapr stop --app-id order-processor
|
||||
```
|
||||
<!-- END_STEP -->
|
|
@ -79,9 +79,8 @@ You're up and running! Both Dapr and your app logs will appear here.
|
|||
== APP == Retrieved Order: "{\"orderId\":3}"
|
||||
== APP == 2023/09/24 23:31:27 Deleted Order: {"orderId":3}
|
||||
```
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Stop and clean up application processes
|
||||
```bash
|
||||
dapr stop --app-id order-processor
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
|
|
@ -76,9 +76,8 @@ You're up and running! Both Dapr and your app logs will appear here.
|
|||
== APP - order-processor == Retrieved Order: {"orderId":2}
|
||||
== APP - order-processor == Deleted Order: {"orderId":2}
|
||||
```
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Stop and clean up application processes
|
||||
```bash
|
||||
dapr stop --app-id order-processor
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
|
|
@ -88,11 +88,10 @@ sleep: 60
|
|||
```bash
|
||||
dapr run --app-id order-processor --resources-path ../../../resources/ -- npm start
|
||||
```
|
||||
<!-- END_STEP -->
|
||||
|
||||
2. Stop and cleanup the process
|
||||
|
||||
```bash
|
||||
dapr stop --app-id order-processor
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
|
|
@ -22,7 +22,7 @@ app.use(bodyParser.json());
|
|||
const daprPort = process.env.DAPR_HTTP_PORT ?? "3500";
|
||||
const daprGRPCPort = process.env.DAPR_GRPC_PORT ?? "50001";
|
||||
|
||||
const stateStoreName = `statestore`;
|
||||
const stateStoreName = process.env.STATE_STORE_NAME ?? "statestore";
|
||||
const stateUrl = `http://localhost:${daprPort}/v1.0/state/${stateStoreName}`;
|
||||
const port = process.env.APP_PORT ?? "3000";
|
||||
|
||||
|
@ -76,4 +76,4 @@ app.get('/ports', (_req, res) => {
|
|||
res.status(200).send({DAPR_HTTP_PORT: daprPort, DAPR_GRPC_PORT: daprGRPCPort })
|
||||
});
|
||||
|
||||
app.listen(port, () => console.log(`Node App listening on port ${port}!`));
|
||||
app.listen(port, () => console.log(`Node App listening on port ${port}!`));
|
||||
|
|
|
@ -33,9 +33,7 @@ host.Start();
|
|||
|
||||
using var daprClient = new DaprClientBuilder().Build();
|
||||
|
||||
// NOTE: WorkflowEngineClient will be replaced with a richer version of DaprClient
|
||||
// in a subsequent SDK release. This is a temporary workaround.
|
||||
WorkflowEngineClient workflowClient = host.Services.GetRequiredService<WorkflowEngineClient>();
|
||||
DaprWorkflowClient workflowClient = host.Services.GetRequiredService<DaprWorkflowClient>();
|
||||
|
||||
// Generate a unique ID for the workflow
|
||||
string orderId = Guid.NewGuid().ToString()[..8];
|
||||
|
@ -51,28 +49,25 @@ OrderPayload orderInfo = new OrderPayload(itemToPurchase, 15000, ammountToPurcha
|
|||
// Start the workflow
|
||||
Console.WriteLine("Starting workflow {0} purchasing {1} {2}", orderId, ammountToPurchase, itemToPurchase);
|
||||
|
||||
await daprClient.StartWorkflowAsync(
|
||||
workflowComponent: DaprWorkflowComponent,
|
||||
workflowName: nameof(OrderProcessingWorkflow),
|
||||
input: orderInfo,
|
||||
instanceId: orderId);
|
||||
await workflowClient.ScheduleNewWorkflowAsync(
|
||||
name: nameof(OrderProcessingWorkflow),
|
||||
instanceId: orderId,
|
||||
input: orderInfo);
|
||||
|
||||
// Wait for the workflow to start and confirm the input
|
||||
GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync(
|
||||
instanceId: orderId,
|
||||
workflowComponent: DaprWorkflowComponent);
|
||||
WorkflowState state = await workflowClient.WaitForWorkflowStartAsync(
|
||||
instanceId: orderId);
|
||||
|
||||
Console.WriteLine("Your workflow has started. Here is the status of the workflow: {0}", state.RuntimeStatus);
|
||||
Console.WriteLine("Your workflow has started. Here is the status of the workflow: {0}", Enum.GetName(typeof(WorkflowRuntimeStatus), state.RuntimeStatus));
|
||||
|
||||
// Wait for the workflow to complete
|
||||
state = await daprClient.WaitForWorkflowCompletionAsync(
|
||||
instanceId: orderId,
|
||||
workflowComponent: DaprWorkflowComponent);
|
||||
state = await workflowClient.WaitForWorkflowCompletionAsync(
|
||||
instanceId: orderId);
|
||||
|
||||
Console.WriteLine("Workflow Status: {0}", state.RuntimeStatus);
|
||||
Console.WriteLine("Workflow Status: {0}", Enum.GetName(typeof(WorkflowRuntimeStatus), state.RuntimeStatus));
|
||||
|
||||
void RestockInventory(string itemToPurchase)
|
||||
{
|
||||
daprClient.SaveStateAsync<OrderPayload>(StoreName, itemToPurchase, new OrderPayload(Name: itemToPurchase, TotalCost: 15000, Quantity: 100));
|
||||
daprClient.SaveStateAsync<OrderPayload>(StoreName, itemToPurchase, new OrderPayload(Name: itemToPurchase, TotalCost: 15000, Quantity: 100));
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,146 @@
|
|||
# Dapr workflows
|
||||
|
||||
In this quickstart, you'll create a simple console application to demonstrate Dapr's workflow programming model and the workflow management API. The console app starts and manages the lifecycle of a workflow that stores and retrieves data in a state store.
|
||||
|
||||
This quickstart includes one project:
|
||||
|
||||
- JavaScript console app `order-processor`
|
||||
|
||||
The quickstart contains 1 workflow to simulate purchasing items from a store, and 5 unique activities within the workflow. These 5 activities are as follows:
|
||||
|
||||
- notifyActivity: This activity utilizes a logger to print out messages throughout the workflow. These messages notify the user when there is insufficient inventory, their payment couldn't be processed, and more.
|
||||
- reserveInventoryActivity: This activity checks the state store to ensure that there is enough inventory present for purchase.
|
||||
- requestApprovalActivity: This activity requests approval for orders over a certain threshold
|
||||
- processPaymentActivity: This activity is responsible for processing and authorizing the payment.
|
||||
- updateInventoryActivity: This activity updates the state store with the new remaining inventory value.
|
||||
|
||||
### Run the order processor workflow with multi-app-run
|
||||
|
||||
1. Open a new terminal window and navigate to `order-processor` directory:
|
||||
|
||||
<!-- STEP
|
||||
name: build order-process app
|
||||
-->
|
||||
|
||||
```bash
|
||||
cd ./javascript/sdk
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
2. Run the console app with Dapr:
|
||||
|
||||
<!-- STEP
|
||||
name: Run order-processor service
|
||||
expected_stdout_lines:
|
||||
- '== APP - workflowApp == == APP == Payment of 100 for 10 item1 processed successfully'
|
||||
- 'there are now 90 item1 in stock'
|
||||
- 'processed successfully!'
|
||||
expected_stderr_lines:
|
||||
output_match_mode: substring
|
||||
background: true
|
||||
sleep: 15
|
||||
timeout_seconds: 120
|
||||
-->
|
||||
|
||||
```bash
|
||||
dapr run -f .
|
||||
```
|
||||
|
||||
<!-- END_STEP -->
|
||||
|
||||
3. Expected output
|
||||
|
||||
|
||||
```
|
||||
== APP - workflowApp == == APP == Orchestration scheduled with ID: 0c332155-1e02-453a-a333-28cfc7777642
|
||||
== APP - workflowApp == == APP == Waiting 30 seconds for instance 0c332155-1e02-453a-a333-28cfc7777642 to complete...
|
||||
== APP - workflowApp == == APP == Received "Orchestrator Request" work item with instance id '0c332155-1e02-453a-a333-28cfc7777642'
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Rebuilding local state with 0 history event...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, EXECUTIONSTARTED=1]
|
||||
== APP - workflowApp == == APP == Processing order 0c332155-1e02-453a-a333-28cfc7777642...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Waiting for 1 task(s) and 0 event(s) to complete...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Returning 1 action(s)
|
||||
== APP - workflowApp == == APP == Received "Activity Request" work item
|
||||
== APP - workflowApp == == APP == Received order 0c332155-1e02-453a-a333-28cfc7777642 for 10 item1 at a total cost of 100
|
||||
== APP - workflowApp == == APP == Activity notifyActivity completed with output undefined (0 chars)
|
||||
== APP - workflowApp == == APP == Received "Orchestrator Request" work item with instance id '0c332155-1e02-453a-a333-28cfc7777642'
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Rebuilding local state with 3 history event...
|
||||
== APP - workflowApp == == APP == Processing order 0c332155-1e02-453a-a333-28cfc7777642...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Waiting for 1 task(s) and 0 event(s) to complete...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Returning 1 action(s)
|
||||
== APP - workflowApp == == APP == Received "Activity Request" work item
|
||||
== APP - workflowApp == == APP == Reserving inventory for 0c332155-1e02-453a-a333-28cfc7777642 of 10 item1
|
||||
== APP - workflowApp == == APP == 2024-02-16T03:15:59.498Z INFO [HTTPClient, HTTPClient] Sidecar Started
|
||||
== APP - workflowApp == == APP == There are 100 item1 in stock
|
||||
== APP - workflowApp == == APP == Activity reserveInventoryActivity completed with output {"success":true,"inventoryItem":{"perItemCost":100,"quantity":100,"itemName":"item1"}} (86 chars)
|
||||
== APP - workflowApp == == APP == Received "Orchestrator Request" work item with instance id '0c332155-1e02-453a-a333-28cfc7777642'
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Rebuilding local state with 6 history event...
|
||||
== APP - workflowApp == == APP == Processing order 0c332155-1e02-453a-a333-28cfc7777642...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Waiting for 1 task(s) and 0 event(s) to complete...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Returning 1 action(s)
|
||||
== APP - workflowApp == == APP == Received "Activity Request" work item
|
||||
== APP - workflowApp == == APP == Processing payment for order item1
|
||||
== APP - workflowApp == == APP == Payment of 100 for 10 item1 processed successfully
|
||||
== APP - workflowApp == == APP == Activity processPaymentActivity completed with output true (4 chars)
|
||||
== APP - workflowApp == == APP == Received "Orchestrator Request" work item with instance id '0c332155-1e02-453a-a333-28cfc7777642'
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Rebuilding local state with 9 history event...
|
||||
== APP - workflowApp == == APP == Processing order 0c332155-1e02-453a-a333-28cfc7777642...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Waiting for 1 task(s) and 0 event(s) to complete...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Returning 1 action(s)
|
||||
== APP - workflowApp == == APP == Received "Activity Request" work item
|
||||
== APP - workflowApp == == APP == Updating inventory for 0c332155-1e02-453a-a333-28cfc7777642 of 10 item1
|
||||
== APP - workflowApp == == APP == Inventory updated for 0c332155-1e02-453a-a333-28cfc7777642, there are now 90 item1 in stock
|
||||
== APP - workflowApp == == APP == Activity updateInventoryActivity completed with output {"success":true,"inventoryItem":{"perItemCost":100,"quantity":90,"itemName":"item1"}} (85 chars)
|
||||
== APP - workflowApp == == APP == Received "Orchestrator Request" work item with instance id '0c332155-1e02-453a-a333-28cfc7777642'
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Rebuilding local state with 12 history event...
|
||||
== APP - workflowApp == == APP == Processing order 0c332155-1e02-453a-a333-28cfc7777642...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Waiting for 1 task(s) and 0 event(s) to complete...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Returning 1 action(s)
|
||||
== APP - workflowApp == == APP == Received "Activity Request" work item
|
||||
== APP - workflowApp == == APP == order 0c332155-1e02-453a-a333-28cfc7777642 processed successfully!
|
||||
== APP - workflowApp == == APP == Activity notifyActivity completed with output undefined (0 chars)
|
||||
== APP - workflowApp == == APP == Received "Orchestrator Request" work item with instance id '0c332155-1e02-453a-a333-28cfc7777642'
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Rebuilding local state with 15 history event...
|
||||
== APP - workflowApp == == APP == Processing order 0c332155-1e02-453a-a333-28cfc7777642...
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Processing 2 new history event(s): [ORCHESTRATORSTARTED=1, TASKCOMPLETED=1]
|
||||
== APP - workflowApp == == APP == Order 0c332155-1e02-453a-a333-28cfc7777642 processed successfully!
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Orchestration completed with status COMPLETED
|
||||
== APP - workflowApp == == APP == 0c332155-1e02-453a-a333-28cfc7777642: Returning 1 action(s)
|
||||
== APP - workflowApp == time="2024-02-15T21:15:59.5589687-06:00" level=info msg="0c332155-1e02-453a-a333-28cfc7777642: 'orderProcessingWorkflow' completed with a COMPLETED status." app_id=activity-sequence-workflow instance=kaibocai-devbox scope=wfengine.backend type=log ver=1.12.4
|
||||
== APP - workflowApp == == APP == Instance 0c332155-1e02-453a-a333-28cfc7777642 completed
|
||||
```
|
||||
|
||||
### View workflow output with Zipkin
|
||||
|
||||
For a more detailed view of the workflow activities (duration, progress etc.), try using Zipkin.
|
||||
|
||||
1. Launch Zipkin container - The [openzipkin/zipkin](https://hub.docker.com/r/openzipkin/zipkin/) docker container is launched on running `dapr init`. Check to make sure the container is running. If it's not, launch the Zipkin docker container with the following command.
|
||||
|
||||
```bash
|
||||
docker run -d -p 9411:9411 openzipkin/zipkin
|
||||
```
|
||||
|
||||
2. View Traces in Zipkin UI - In your browser go to http://localhost:9411 to view the workflow trace spans in the Zipkin web UI. The order-processor workflow should be viewable with the following output in the Zipkin web UI.
|
||||
|
||||
<img src="img/workflow-trace-spans-zipkin.png">
|
||||
|
||||
### What happened?
|
||||
|
||||
When you ran `dapr run --app-id activity-sequence-workflow --app-protocol grpc --dapr-grpc-port 50001 --components-path ../../components npm run start:order-process`
|
||||
|
||||
1. A unique order ID for the workflow is generated (in the above example, `0c332155-1e02-453a-a333-28cfc7777642`) and the workflow is scheduled.
|
||||
2. The `notifyActivity` workflow activity sends a notification saying an order for 10 cars has been received.
|
||||
3. The `reserveInventoryActivity` workflow activity checks the inventory data, determines if you can supply the ordered item, and responds with the number of cars in stock.
|
||||
4. Your workflow starts and notifies you of its status.
|
||||
5. The `requestApprovalActivity` workflow activity requests approval for order `0c332155-1e02-453a-a333-28cfc7777642`
|
||||
6. The `processPaymentActivity` workflow activity begins processing payment for order `0c332155-1e02-453a-a333-28cfc7777642` and confirms if successful.
|
||||
7. The `updateInventoryActivity` workflow activity updates the inventory with the current available cars after the order has been processed.
|
||||
8. The `notifyActivity` workflow activity sends a notification saying that order `0c332155-1e02-453a-a333-28cfc7777642` has completed and processed.
|
||||
9. The workflow terminates as completed and processed.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
version: 1
|
||||
common:
|
||||
resourcesPath: ../../components
|
||||
apps:
|
||||
- appID: workflowApp
|
||||
appDirPath: ./order-processor/
|
||||
command: ["npm", "run", "start:dapr:order-process"]
|
|
@ -0,0 +1,2 @@
|
|||
include ../../../docker.mk
|
||||
include ../../../validate.mk
|
|
@ -0,0 +1,107 @@
|
|||
export class OrderPayload {
|
||||
itemName: string;
|
||||
totalCost: number;
|
||||
quantity: number;
|
||||
|
||||
constructor(itemName: string, totalCost: number, quantity: number) {
|
||||
this.itemName = itemName;
|
||||
this.totalCost = totalCost;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
toJson(): string {
|
||||
return `{"itemName": "${this.itemName}", "quantity": ${this.quantity}, "totalCost": ${this.totalCost}}`;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `OrderPayload(name=${this.itemName}, totalCost=${this.totalCost}, quantity=${this.quantity})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class InventoryItem {
|
||||
itemName: string;
|
||||
perItemCost: number;
|
||||
quantity: number;
|
||||
|
||||
constructor(itemName: string, perItemCost: number, quantity: number) {
|
||||
this.itemName = itemName;
|
||||
this.perItemCost = perItemCost;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `InventoryItem(itemName=${this.itemName}, perItemCost=${this.perItemCost}, quantity=${this.quantity})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class InventoryRequest {
|
||||
requestId: string;
|
||||
itemName: string;
|
||||
quantity: number;
|
||||
|
||||
constructor(requestId: string, itemName: string, quantity: number) {
|
||||
this.requestId = requestId;
|
||||
this.itemName = itemName;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `InventoryRequest(requestId=${this.requestId}, itemName=${this.itemName}, quantity=${this.quantity})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class InventoryResult {
|
||||
success: boolean;
|
||||
inventoryItem?: InventoryItem;
|
||||
|
||||
constructor(success: boolean, inventoryItem: InventoryItem | undefined) {
|
||||
this.success = success;
|
||||
this.inventoryItem = inventoryItem;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `InventoryResult(success=${this.success}, inventoryItem=${this.inventoryItem})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class OrderPaymentRequest {
|
||||
requestId: string;
|
||||
itemBeingPurchased: string;
|
||||
amount: number;
|
||||
quantity: number;
|
||||
|
||||
constructor(requestId: string, itemBeingPurchased: string, amount: number, quantity: number) {
|
||||
this.requestId = requestId;
|
||||
this.itemBeingPurchased = itemBeingPurchased;
|
||||
this.amount = amount;
|
||||
this.quantity = quantity;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `PaymentRequest(requestId=${this.requestId}, itemBeingPurchased=${this.itemBeingPurchased}, amount=${this.amount}, quantity=${this.quantity})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class ApprovalRequired {
|
||||
approval: boolean;
|
||||
|
||||
constructor(approval: boolean) {
|
||||
this.approval = approval;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `ApprovalRequired(approval=${this.approval})`;
|
||||
}
|
||||
}
|
||||
|
||||
export class OrderNotification {
|
||||
message: string;
|
||||
|
||||
constructor(message: string) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return `Notification(message=${this.message})`;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
import { WorkflowActivityContext, WorkflowContext, TWorkflow, DaprClient } from "@dapr/dapr-dev";
|
||||
import { InventoryItem, InventoryRequest, InventoryResult, OrderNotification, OrderPayload, OrderPaymentRequest } from "./model";
|
||||
|
||||
const daprClient = new DaprClient();
|
||||
const storeName = "statestore";
|
||||
|
||||
// Defines Notify Activity. This is used by the workflow to send out a notification
|
||||
export const notifyActivity = async (_: WorkflowActivityContext, orderNotification: OrderNotification) => {
|
||||
console.log(orderNotification.message);
|
||||
return;
|
||||
};
|
||||
|
||||
//Defines Reserve Inventory Activity. This is used by the workflow to verify if inventory is available for the order
|
||||
export const reserveInventoryActivity = async (_: WorkflowActivityContext, inventoryRequest: InventoryRequest) => {
|
||||
console.log(`Reserving inventory for ${inventoryRequest.requestId} of ${inventoryRequest.quantity} ${inventoryRequest.itemName}`);
|
||||
const result = await daprClient.state.get(storeName, inventoryRequest.itemName);
|
||||
if (result == undefined || result == null) {
|
||||
return new InventoryResult(false, undefined);
|
||||
}
|
||||
const inventoryItem = result as InventoryItem;
|
||||
console.log(`There are ${inventoryItem.quantity} ${inventoryItem.itemName} in stock`);
|
||||
|
||||
if (inventoryItem.quantity >= inventoryRequest.quantity) {
|
||||
return new InventoryResult(true, inventoryItem)
|
||||
}
|
||||
return new InventoryResult(false, undefined);
|
||||
}
|
||||
|
||||
export const requestApprovalActivity = async (_: WorkflowActivityContext, orderPayLoad: OrderPayload) => {
|
||||
console.log(`Requesting approval for order ${orderPayLoad.itemName}`);
|
||||
return true;
|
||||
}
|
||||
|
||||
export const processPaymentActivity = async (_: WorkflowActivityContext, orderPaymentRequest: OrderPaymentRequest) => {
|
||||
console.log(`Processing payment for order ${orderPaymentRequest.itemBeingPurchased}`);
|
||||
console.log(`Payment of ${orderPaymentRequest.amount} for ${orderPaymentRequest.quantity} ${orderPaymentRequest.itemBeingPurchased} processed successfully`);
|
||||
return true;
|
||||
}
|
||||
|
||||
export const updateInventoryActivity = async (_: WorkflowActivityContext, inventoryRequest: InventoryRequest) => {
|
||||
console.log(`Updating inventory for ${inventoryRequest.requestId} of ${inventoryRequest.quantity} ${inventoryRequest.itemName}`);
|
||||
const result = await daprClient.state.get(storeName, inventoryRequest.itemName);
|
||||
if (result == undefined || result == null) {
|
||||
return new InventoryResult(false, undefined);
|
||||
}
|
||||
const inventoryItem = result as InventoryItem;
|
||||
inventoryItem.quantity = inventoryItem.quantity - inventoryRequest.quantity;
|
||||
if (inventoryItem.quantity < 0) {
|
||||
console.log(`Insufficient inventory for ${inventoryRequest.requestId} of ${inventoryRequest.quantity} ${inventoryRequest.itemName}`);
|
||||
return new InventoryResult(false, undefined);
|
||||
}
|
||||
await daprClient.state.save(storeName, [
|
||||
{
|
||||
key: inventoryRequest.itemName,
|
||||
value: inventoryItem,
|
||||
}
|
||||
]);
|
||||
console.log(`Inventory updated for ${inventoryRequest.requestId}, there are now ${inventoryItem.quantity} ${inventoryItem.itemName} in stock`);
|
||||
return new InventoryResult(true, inventoryItem);
|
||||
}
|
||||
|
||||
export const orderProcessingWorkflow: TWorkflow = async function* (ctx: WorkflowContext, orderPayLoad: OrderPayload): any {
|
||||
const orderId = ctx.getWorkflowInstanceId();
|
||||
console.log(`Processing order ${orderId}...`);
|
||||
|
||||
const orderNotification: OrderNotification = {
|
||||
message: `Received order ${orderId} for ${orderPayLoad.quantity} ${orderPayLoad.itemName} at a total cost of ${orderPayLoad.totalCost}`,
|
||||
};
|
||||
yield ctx.callActivity(notifyActivity, orderNotification);
|
||||
|
||||
const inventoryRequest = new InventoryRequest(orderId, orderPayLoad.itemName, orderPayLoad.quantity);
|
||||
const inventoryResult = yield ctx.callActivity(reserveInventoryActivity, inventoryRequest);
|
||||
|
||||
if (!inventoryResult.success) {
|
||||
const orderNotification: OrderNotification = {
|
||||
message: `Insufficient inventory for order ${orderId}`,
|
||||
};
|
||||
yield ctx.callActivity(notifyActivity, orderNotification);
|
||||
return;
|
||||
}
|
||||
|
||||
if (orderPayLoad.totalCost > 5000) {
|
||||
const approvalResult = yield ctx.callActivity(requestApprovalActivity, orderPayLoad);
|
||||
if (!approvalResult) {
|
||||
const orderNotification: OrderNotification = {
|
||||
message: `Order ${orderId} approval denied`,
|
||||
};
|
||||
yield ctx.callActivity(notifyActivity, orderNotification);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const orderPaymentRequest = new OrderPaymentRequest(orderId, orderPayLoad.itemName, orderPayLoad.totalCost, orderPayLoad.quantity);
|
||||
const paymentResult = yield ctx.callActivity(processPaymentActivity, orderPaymentRequest);
|
||||
|
||||
if (!paymentResult) {
|
||||
const orderNotification: OrderNotification = {
|
||||
message: `Payment for order ${orderId} failed`,
|
||||
};
|
||||
yield ctx.callActivity(notifyActivity, orderNotification);
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedResult = yield ctx.callActivity(updateInventoryActivity, inventoryRequest);
|
||||
if (!updatedResult.success) {
|
||||
const orderNotification: OrderNotification = {
|
||||
message: `Failed to update inventory for order ${orderId}`,
|
||||
};
|
||||
yield ctx.callActivity(notifyActivity, orderNotification);
|
||||
return;
|
||||
}
|
||||
|
||||
const orderCompletedNotification: OrderNotification = {
|
||||
message: `order ${orderId} processed successfully!`,
|
||||
};
|
||||
yield ctx.callActivity(notifyActivity, orderCompletedNotification);
|
||||
|
||||
console.log(`Order ${orderId} processed successfully!`);
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
import { DaprWorkflowClient, WorkflowRuntime, DaprClient } from "@dapr/dapr-dev";
|
||||
import { InventoryItem, OrderPayload } from "./model";
|
||||
import { notifyActivity, orderProcessingWorkflow, processPaymentActivity, requestApprovalActivity, reserveInventoryActivity, updateInventoryActivity } from "./orderProcessingWorkflow";
|
||||
|
||||
async function start() {
|
||||
// Update the gRPC client and worker to use a local address and port
|
||||
const workflowClient = new DaprWorkflowClient();
|
||||
const workflowWorker = new WorkflowRuntime();
|
||||
|
||||
const daprClient = new DaprClient();
|
||||
const storeName = "statestore";
|
||||
|
||||
const inventory = new InventoryItem("item1", 100, 100);
|
||||
const key = inventory.itemName;
|
||||
|
||||
await daprClient.state.save(storeName, [
|
||||
{
|
||||
key: key,
|
||||
value: inventory,
|
||||
}
|
||||
]);
|
||||
|
||||
const order = new OrderPayload("item1", 100, 10);
|
||||
|
||||
workflowWorker
|
||||
.registerWorkflow(orderProcessingWorkflow)
|
||||
.registerActivity(notifyActivity)
|
||||
.registerActivity(reserveInventoryActivity)
|
||||
.registerActivity(requestApprovalActivity)
|
||||
.registerActivity(processPaymentActivity)
|
||||
.registerActivity(updateInventoryActivity);
|
||||
|
||||
// Wrap the worker startup in a try-catch block to handle any errors during startup
|
||||
try {
|
||||
await workflowWorker.start();
|
||||
console.log("Workflow runtime started successfully");
|
||||
} catch (error) {
|
||||
console.error("Error starting workflow runtime:", error);
|
||||
}
|
||||
|
||||
// Schedule a new orchestration
|
||||
try {
|
||||
const id = await workflowClient.scheduleNewWorkflow(orderProcessingWorkflow, order);
|
||||
console.log(`Orchestration scheduled with ID: ${id}`);
|
||||
|
||||
// Wait for orchestration completion
|
||||
const state = await workflowClient.waitForWorkflowCompletion(id, undefined, 30);
|
||||
|
||||
console.log(`Orchestration completed! Result: ${state?.serializedOutput}`);
|
||||
} catch (error) {
|
||||
console.error("Error scheduling or waiting for orchestration:", error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
await workflowWorker.stop();
|
||||
await workflowClient.stop();
|
||||
}
|
||||
|
||||
start().catch((e) => {
|
||||
console.error(e);
|
||||
process.exit(1);
|
||||
});
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "dapr-example-workflow-authoring",
|
||||
"version": "1.0.0",
|
||||
"description": "An example utilizing the Dapr JS-SDK to author workflow",
|
||||
"private": "true",
|
||||
"scripts": {
|
||||
"build": "npx tsc --outDir ./dist/",
|
||||
"start:order-process": "npm run build && node dist/workflowApp.js",
|
||||
"start:dapr:order-process": "dapr run --app-id activity-sequence-workflow --app-protocol grpc --dapr-grpc-port 50001 --components-path ../../components npm run start:order-process"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/readline-sync": "^1.4.8",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.0.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dapr/dapr-dev": "3.2.0-20240212030427-319e2fb",
|
||||
"@types/node": "^18.16.3"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "ES2020" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
// "allowJs": true, /* Allow javascript files to be compiled. */
|
||||
// "checkJs": true, /* Report errors in .js files. */
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
"outDir": "./dist" /* Redirect output structure to the directory. */,
|
||||
"rootDir": "./order-processor" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
// "noEmit": true, /* Do not emit outputs. */
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* Enable strict null checks. */
|
||||
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
|
||||
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
|
||||
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
|
||||
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
|
||||
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
|
||||
|
||||
/* Additional Checks */
|
||||
// "noUnusedLocals": true, /* Report errors on unused locals. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
|
||||
/* Advanced Options */
|
||||
"skipLibCheck": true /* Skip type checking of declaration files. */,
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
|
||||
}
|
||||
}
|
|
@ -49,16 +49,11 @@ dapr run -f .
|
|||
|
||||
```
|
||||
==========Begin the purchase of item:==========
|
||||
To restock items, type 'restock'.
|
||||
To exit workflow console app, type 'exit'.
|
||||
Enter the name of one of the following items to order: paperclip, cars, computers: cars
|
||||
How many cars would you like to purchase? 10
|
||||
Starting order workflow, purchasing 10 of cars
|
||||
INFO:NotifyActivity:Received order b903d749cd814e099f06ebf4a56a2f90 for 10 cars at $150000 !
|
||||
INFO:VerifyInventoryActivity:Verifying inventory for order b903d749cd814e099f06ebf4a56a2f90 of 10 cars
|
||||
INFO:VerifyInventoryActivity:There are 100 Cars available for purchase
|
||||
INFO:RequestApprovalActivity:Requesting approval for payment of 150000 USD for 10 cars
|
||||
(ID = b903d749cd814e099f06ebf4a56a2f90) requires approval. Approve? [Y/N] y
|
||||
INFO:NotifyActivity:Payment for order b903d749cd814e099f06ebf4a56a2f90 has been approved!
|
||||
INFO:ProcessPaymentActivity:Processing payment: b903d749cd814e099f06ebf4a56a2f90 for 10 cars at 150000 USD
|
||||
INFO:ProcessPaymentActivity:Payment for request ID b903d749cd814e099f06ebf4a56a2f90 processed successfully
|
||||
|
|
Loading…
Reference in New Issue