Skip to content

Commit 4ce700f

Browse files
authored
Merge branch 'main' into fix/pubsub_re_sub
2 parents 00222e1 + 853d60f commit 4ce700f

File tree

8 files changed

+717
-13
lines changed

8 files changed

+717
-13
lines changed

examples/demo_actor/README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,10 @@ This document describes how to create an Actor(DemoActor) and invoke its methods
1717

1818
You can install dapr SDK package using pip command:
1919

20-
<!-- STEP
21-
name: Install requirements
22-
-->
23-
2420
```sh
2521
pip3 install -r demo_actor/requirements.txt
2622
```
2723

28-
<!-- END_STEP -->
29-
3024
## Run in self-hosted mode
3125

3226
<!-- STEP

examples/demo_workflow/README.md

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,10 @@ It demonstrates the following APIs:
1818

1919
You can install dapr SDK package using pip command:
2020

21-
<!-- STEP
22-
name: Install requirements
23-
-->
24-
2521
```sh
2622
pip3 install -r demo_workflow/requirements.txt
2723
```
2824

29-
<!-- END_STEP -->
30-
3125
<!-- STEP
3226
name: Running this example
3327
expected_stdout_lines:

examples/workflow/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,72 @@ The output of this example should look like this:
8585
- "== APP == Workflow completed! Result: Completed"
8686
```
8787

88+
### Simple Workflow with async workflow client
89+
This example represents a workflow that manages counters through a series of activities and child workflows. It features using the async workflow client.
90+
It shows several Dapr Workflow features including:
91+
- Basic activity execution with counter increments
92+
- Retryable activities with configurable retry policies
93+
- Child workflow orchestration with retry logic
94+
- External event handling with timeouts
95+
- Workflow state management (pause/resume)
96+
- Activity error handling and retry backoff
97+
- Global state tracking across workflow components
98+
- Workflow lifecycle management (start, pause, resume, purge)
99+
100+
<!--STEP
101+
name: Run the simple workflow example
102+
expected_stdout_lines:
103+
- "== APP == Hi Counter!"
104+
- "== APP == New counter value is: 1!"
105+
- "== APP == New counter value is: 11!"
106+
- "== APP == Retry count value is: 0!"
107+
- "== APP == Retry count value is: 1! This print statement verifies retry"
108+
- "== APP == Appending 1 to child_orchestrator_string!"
109+
- "== APP == Appending a to child_orchestrator_string!"
110+
- "== APP == Appending a to child_orchestrator_string!"
111+
- "== APP == Appending 2 to child_orchestrator_string!"
112+
- "== APP == Appending b to child_orchestrator_string!"
113+
- "== APP == Appending b to child_orchestrator_string!"
114+
- "== APP == Appending 3 to child_orchestrator_string!"
115+
- "== APP == Appending c to child_orchestrator_string!"
116+
- "== APP == Appending c to child_orchestrator_string!"
117+
- "== APP == Get response from hello_world_wf after pause call: SUSPENDED"
118+
- "== APP == Get response from hello_world_wf after resume call: RUNNING"
119+
- "== APP == New counter value is: 111!"
120+
- "== APP == New counter value is: 1111!"
121+
- "== APP == Workflow completed! Result: Completed"
122+
timeout_seconds: 30
123+
-->
124+
125+
```sh
126+
dapr run --app-id wf-simple-aio-example -- python3 simple_aio_client.py
127+
```
128+
<!--END_STEP-->
129+
130+
The output of this example should look like this:
131+
132+
```
133+
- "== APP == Hi Counter!"
134+
- "== APP == New counter value is: 1!"
135+
- "== APP == New counter value is: 11!"
136+
- "== APP == Retry count value is: 0!"
137+
- "== APP == Retry count value is: 1! This print statement verifies retry"
138+
- "== APP == Appending 1 to child_orchestrator_string!"
139+
- "== APP == Appending a to child_orchestrator_string!"
140+
- "== APP == Appending a to child_orchestrator_string!"
141+
- "== APP == Appending 2 to child_orchestrator_string!"
142+
- "== APP == Appending b to child_orchestrator_string!"
143+
- "== APP == Appending b to child_orchestrator_string!"
144+
- "== APP == Appending 3 to child_orchestrator_string!"
145+
- "== APP == Appending c to child_orchestrator_string!"
146+
- "== APP == Appending c to child_orchestrator_string!"
147+
- "== APP == Get response from hello_world_wf after pause call: SUSPENDED"
148+
- "== APP == Get response from hello_world_wf after resume call: RUNNING"
149+
- "== APP == New counter value is: 111!"
150+
- "== APP == New counter value is: 1111!"
151+
- "== APP == Workflow completed! Result: Completed"
152+
```
153+
88154
### Task Chaining
89155

90156
This example demonstrates how to chain "activity" tasks together in a workflow. You can run this sample using the following command:
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright 2025 The Dapr Authors
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
# http://www.apache.org/licenses/LICENSE-2.0
7+
# Unless required by applicable law or agreed to in writing, software
8+
# distributed under the License is distributed on an "AS IS" BASIS,
9+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10+
# See the License for the specific language governing permissions and
11+
# limitations under the License.
12+
import asyncio
13+
from datetime import timedelta
14+
15+
from dapr.ext.workflow import (
16+
DaprWorkflowContext,
17+
RetryPolicy,
18+
WorkflowActivityContext,
19+
WorkflowRuntime,
20+
when_any,
21+
)
22+
from dapr.ext.workflow.aio import DaprWorkflowClient
23+
24+
from dapr.clients.exceptions import DaprInternalError
25+
from dapr.conf import Settings
26+
27+
settings = Settings()
28+
29+
counter = 0
30+
retry_count = 0
31+
child_orchestrator_count = 0
32+
child_orchestrator_string = ''
33+
child_act_retry_count = 0
34+
instance_id = 'exampleInstanceID'
35+
child_instance_id = 'childInstanceID'
36+
workflow_name = 'hello_world_wf'
37+
child_workflow_name = 'child_wf'
38+
input_data = 'Hi Counter!'
39+
event_name = 'event1'
40+
event_data = 'eventData'
41+
non_existent_id_error = 'no such instance exists'
42+
43+
retry_policy = RetryPolicy(
44+
first_retry_interval=timedelta(seconds=1),
45+
max_number_of_attempts=3,
46+
backoff_coefficient=2,
47+
max_retry_interval=timedelta(seconds=10),
48+
retry_timeout=timedelta(seconds=100),
49+
)
50+
51+
wfr = WorkflowRuntime()
52+
53+
54+
@wfr.workflow(name='hello_world_wf')
55+
def hello_world_wf(ctx: DaprWorkflowContext, wf_input):
56+
print(f'{wf_input}')
57+
yield ctx.call_activity(hello_act, input=1)
58+
yield ctx.call_activity(hello_act, input=10)
59+
yield ctx.call_activity(hello_retryable_act, retry_policy=retry_policy)
60+
yield ctx.call_child_workflow(child_retryable_wf, retry_policy=retry_policy)
61+
62+
# Change in event handling: Use when_any to handle both event and timeout
63+
event = ctx.wait_for_external_event(event_name)
64+
timeout = ctx.create_timer(timedelta(seconds=30))
65+
winner = yield when_any([event, timeout])
66+
67+
if winner == timeout:
68+
print('Workflow timed out waiting for event')
69+
return 'Timeout'
70+
71+
yield ctx.call_activity(hello_act, input=100)
72+
yield ctx.call_activity(hello_act, input=1000)
73+
return 'Completed'
74+
75+
76+
@wfr.activity(name='hello_act')
77+
def hello_act(ctx: WorkflowActivityContext, wf_input):
78+
global counter
79+
counter += wf_input
80+
print(f'New counter value is: {counter}!', flush=True)
81+
82+
83+
@wfr.activity(name='hello_retryable_act')
84+
def hello_retryable_act(ctx: WorkflowActivityContext):
85+
global retry_count
86+
if (retry_count % 2) == 0:
87+
print(f'Retry count value is: {retry_count}!', flush=True)
88+
retry_count += 1
89+
raise ValueError('Retryable Error')
90+
print(f'Retry count value is: {retry_count}! This print statement verifies retry', flush=True)
91+
retry_count += 1
92+
93+
94+
@wfr.workflow(name='child_retryable_wf')
95+
def child_retryable_wf(ctx: DaprWorkflowContext):
96+
global child_orchestrator_string, child_orchestrator_count
97+
if not ctx.is_replaying:
98+
child_orchestrator_count += 1
99+
print(f'Appending {child_orchestrator_count} to child_orchestrator_string!', flush=True)
100+
child_orchestrator_string += str(child_orchestrator_count)
101+
yield ctx.call_activity(
102+
act_for_child_wf, input=child_orchestrator_count, retry_policy=retry_policy
103+
)
104+
if child_orchestrator_count < 3:
105+
raise ValueError('Retryable Error')
106+
107+
108+
@wfr.activity(name='act_for_child_wf')
109+
def act_for_child_wf(ctx: WorkflowActivityContext, inp):
110+
global child_orchestrator_string, child_act_retry_count
111+
inp_char = chr(96 + inp)
112+
print(f'Appending {inp_char} to child_orchestrator_string!', flush=True)
113+
child_orchestrator_string += inp_char
114+
if child_act_retry_count % 2 == 0:
115+
child_act_retry_count += 1
116+
raise ValueError('Retryable Error')
117+
child_act_retry_count += 1
118+
119+
120+
async def main():
121+
wfr.start()
122+
wf_client = DaprWorkflowClient()
123+
124+
try:
125+
print('==========Start Counter Increase as per Input:==========')
126+
await wf_client.schedule_new_workflow(
127+
workflow=hello_world_wf, input=input_data, instance_id=instance_id
128+
)
129+
130+
await wf_client.wait_for_workflow_start(instance_id)
131+
132+
# Sleep to let the workflow run initial activities
133+
await asyncio.sleep(12)
134+
135+
assert counter == 11
136+
assert retry_count == 2
137+
assert child_orchestrator_string == '1aa2bb3cc'
138+
139+
# Pause Test
140+
await wf_client.pause_workflow(instance_id=instance_id)
141+
metadata = await wf_client.get_workflow_state(instance_id=instance_id)
142+
print(f'Get response from {workflow_name} after pause call: {metadata.runtime_status.name}')
143+
144+
# Resume Test
145+
await wf_client.resume_workflow(instance_id=instance_id)
146+
metadata = await wf_client.get_workflow_state(instance_id=instance_id)
147+
print(
148+
f'Get response from {workflow_name} after resume call: {metadata.runtime_status.name}'
149+
)
150+
151+
await asyncio.sleep(2) # Give the workflow time to reach the event wait state
152+
await wf_client.raise_workflow_event(
153+
instance_id=instance_id, event_name=event_name, data=event_data
154+
)
155+
156+
print('========= Waiting for Workflow completion', flush=True)
157+
try:
158+
state = await wf_client.wait_for_workflow_completion(instance_id, timeout_in_seconds=30)
159+
if state.runtime_status.name == 'COMPLETED':
160+
print('Workflow completed! Result: {}'.format(state.serialized_output.strip('"')))
161+
else:
162+
print(f'Workflow failed! Status: {state.runtime_status.name}')
163+
except TimeoutError:
164+
print('*** Workflow timed out!')
165+
166+
await wf_client.purge_workflow(instance_id=instance_id)
167+
try:
168+
await wf_client.get_workflow_state(instance_id=instance_id)
169+
except DaprInternalError as err:
170+
if non_existent_id_error in err._message:
171+
print('Instance Successfully Purged')
172+
finally:
173+
wfr.shutdown()
174+
175+
176+
if __name__ == '__main__':
177+
asyncio.run(main())
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# -*- coding: utf-8 -*-
2+
3+
"""
4+
Copyright 2025 The Dapr Authors
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.
14+
"""
15+
16+
from .dapr_workflow_client import DaprWorkflowClient
17+
18+
__all__ = [
19+
'DaprWorkflowClient',
20+
]

0 commit comments

Comments
 (0)