Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 4ef3a37

Browse files
authored
Porting to Python, Java Polling Examples (temporalio#68)
* Porting to Python, Java Polling Examples * updates code sample * updates code * remove attempts * Update polling/periodic_sequence/workflows.py * tighten readmes remove extra fluff from activity * adds other fixes * adding external service
1 parent f62501f commit 4ef3a37

22 files changed

+349
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ Some examples require extra dependencies. See each sample's directory for specif
5858
* [encryption](encryption) - Apply end-to-end encryption for all input/output.
5959
* [open_telemetry](open_telemetry) - Trace workflows with OpenTelemetry.
6060
* [patching](patching) - Alter workflows safely with `patch` and `deprecate_patch`.
61+
* [polling](polling) - Recommended implementation of an activity that needs to periodically poll an external resource waiting its successful completion.
6162
* [prometheus](prometheus) - Configure Prometheus metrics on clients/workers.
6263
* [pydantic_converter](pydantic_converter) - Data converter for using Pydantic models.
6364
* [schedules](schedules) - Demonstrates a Workflow Execution that occurs according to a schedule.

polling/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Polling
2+
3+
These samples show three different best practices for polling.
4+
5+
1. [Frequently Polling Activity](./frequent/README.md)
6+
2. [Infrequently Polling Activity](./infrequent/README.md)
7+
3. [Periodic Polling of a Sequence of Activities](./periodic_sequence/README.md)
8+
9+
The samples are based on [this](https://community.temporal.io/t/what-is-the-best-practice-for-a-polling-activity/328/2) community forum thread.

polling/__init__.py

Whitespace-only changes.

polling/frequent/README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Frequently Polling Activity
2+
3+
This sample shows how we can implement frequent polling (1 second or faster) inside our Activity. The implementation is a loop that polls our service and then sleeps for the poll interval (1 second in the sample).
4+
5+
To ensure that polling Activity is restarted in a timely manner, we make sure that it heartbeats on every iteration. Note that heartbeating only works if we set the `heartbeat_timeout` to a shorter value than the Activity `start_to_close_timeout` timeout.
6+
7+
To run, first see [README.md](../../README.md) for prerequisites.
8+
9+
Then, run the following from this directory to run the sample:
10+
11+
```bash
12+
poetry run python run_worker.py
13+
poetry run python run_frequent.py
14+
```
15+
16+
The Workflow will continue to poll the service and heartbeat on every iteration until it succeeds.
17+
18+
Note that with frequent polling, the Activity may execute for a long time, and it may be beneficial to set a Timeout on the Activity to avoid long-running Activities that are not heartbeating.
19+
20+
If the polling interval needs to be changed during runtime, the Activity needs to be canceled and a new instance with the updated arguments needs to be started.

polling/frequent/__init__.py

Whitespace-only changes.

polling/frequent/activities.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import asyncio
2+
from dataclasses import dataclass
3+
4+
from temporalio import activity
5+
6+
from polling.test_service import TestService
7+
8+
9+
@dataclass
10+
class ComposeGreetingInput:
11+
greeting: str
12+
name: str
13+
14+
15+
@activity.defn
16+
async def compose_greeting(input: ComposeGreetingInput) -> str:
17+
test_service = TestService()
18+
while True:
19+
try:
20+
result = test_service.get_service_result(input)
21+
return result
22+
except Exception:
23+
activity.heartbeat("Invoking activity")

polling/frequent/run_frequent.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import asyncio
2+
3+
from temporalio.client import Client
4+
from workflows import GreetingWorkflow
5+
6+
7+
async def main():
8+
client = await Client.connect("localhost:7233")
9+
result = await client.execute_workflow(
10+
GreetingWorkflow.run,
11+
"World",
12+
id="frequent-activity-retry",
13+
task_queue="frequent-activity-retry-task-queue",
14+
)
15+
print(f"Result: {result}")
16+
17+
18+
if __name__ == "__main__":
19+
asyncio.run(main())

polling/frequent/run_worker.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import asyncio
2+
3+
from activities import compose_greeting
4+
from temporalio.client import Client
5+
from temporalio.worker import Worker
6+
from workflows import GreetingWorkflow
7+
8+
9+
async def main():
10+
client = await Client.connect("localhost:7233")
11+
12+
worker = Worker(
13+
client,
14+
task_queue="frequent-activity-retry-task-queue",
15+
workflows=[GreetingWorkflow],
16+
activities=[compose_greeting],
17+
)
18+
await worker.run()
19+
20+
21+
if __name__ == "__main__":
22+
asyncio.run(main())

polling/frequent/workflows.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from datetime import timedelta
2+
3+
from temporalio import workflow
4+
5+
with workflow.unsafe.imports_passed_through():
6+
from activities import ComposeGreetingInput, compose_greeting
7+
8+
9+
@workflow.defn
10+
class GreetingWorkflow:
11+
@workflow.run
12+
async def run(self, name: str) -> str:
13+
return await workflow.execute_activity(
14+
compose_greeting,
15+
ComposeGreetingInput("Hello", name),
16+
start_to_close_timeout=timedelta(seconds=60),
17+
heartbeat_timeout=timedelta(seconds=2),
18+
)

polling/infrequent/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Infrequently Polling Activity
2+
3+
This sample shows how to use Activity retries for infrequent polling of a third-party service (for example via REST). This method can be used for infrequent polls of one minute or slower.
4+
5+
Activity retries are utilized for this option, setting the following Retry options:
6+
7+
- `backoff_coefficient`: to 1
8+
- `initial_interval`: to the polling interval (in this sample set to 60 seconds)
9+
10+
This will enable the Activity to be retried exactly on the set interval.
11+
12+
To run, first see [README.md](../../README.md) for prerequisites.
13+
14+
Then, run the following from this directory to run the sample:
15+
16+
```bash
17+
poetry run python run_worker.py
18+
poetry run python run_infrequent.py
19+
```
20+
21+
Since the test service simulates being _down_ for four polling attempts and then returns _OK_ on the fifth poll attempt, the Workflow will perform four Activity retries with a 60-second poll interval, and then return the service result on the successful fifth attempt.
22+
23+
Note that individual Activity retries are not recorded in Workflow History, so this approach can poll for a very long time without affecting the history size.

polling/infrequent/__init__.py

Whitespace-only changes.

polling/infrequent/activities.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import asyncio
2+
from dataclasses import dataclass
3+
4+
from temporalio import activity
5+
6+
from polling.test_service import TestService
7+
8+
9+
@dataclass
10+
class ComposeGreetingInput:
11+
greeting: str
12+
name: str
13+
14+
15+
@activity.defn
16+
async def compose_greeting(input: ComposeGreetingInput) -> str:
17+
test_service = TestService()
18+
while True:
19+
try:
20+
result = test_service.get_service_result(input)
21+
return result
22+
except Exception:
23+
activity.heartbeat("Invoking activity")

polling/infrequent/run_infrequent.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import asyncio
2+
3+
from temporalio.client import Client
4+
from workflows import GreetingWorkflow
5+
6+
7+
async def main():
8+
client = await Client.connect("localhost:7233")
9+
result = await client.execute_workflow(
10+
GreetingWorkflow.run,
11+
"World",
12+
id="infrequent-activity-retry",
13+
task_queue="infrequent-activity-retry-task-queue",
14+
)
15+
print(f"Result: {result}")
16+
17+
18+
if __name__ == "__main__":
19+
asyncio.run(main())

polling/infrequent/run_worker.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import asyncio
2+
3+
from activities import compose_greeting
4+
from temporalio.client import Client
5+
from temporalio.worker import Worker
6+
from workflows import GreetingWorkflow
7+
8+
9+
async def main():
10+
client = await Client.connect("localhost:7233")
11+
12+
worker = Worker(
13+
client,
14+
task_queue="infrequent-activity-retry-task-queue",
15+
workflows=[GreetingWorkflow],
16+
activities=[compose_greeting],
17+
)
18+
await worker.run()
19+
20+
21+
if __name__ == "__main__":
22+
asyncio.run(main())

polling/infrequent/workflows.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from datetime import timedelta
2+
3+
from temporalio import workflow
4+
from temporalio.common import RetryPolicy
5+
6+
with workflow.unsafe.imports_passed_through():
7+
from activities import ComposeGreetingInput, compose_greeting
8+
9+
10+
@workflow.defn
11+
class GreetingWorkflow:
12+
@workflow.run
13+
async def run(self, name: str) -> str:
14+
return await workflow.execute_activity(
15+
compose_greeting,
16+
ComposeGreetingInput("Hello", name),
17+
start_to_close_timeout=timedelta(seconds=2),
18+
retry_policy=RetryPolicy(
19+
backoff_coefficient=1.0,
20+
initial_interval=timedelta(seconds=60),
21+
),
22+
)

polling/periodic_sequence/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Periodic Polling of a Sequence of Activities
2+
3+
This sample demonstrates how to use a Child Workflow for periodic Activity polling.
4+
5+
This is a rare scenario where polling requires execution of a Sequence of Activities, or Activity arguments need to change between polling retries. For this case we use a Child Workflow to call polling activities a set number of times in a loop and then periodically call Continue-As-New.
6+
7+
To run, first see [README.md](../../README.md) for prerequisites.
8+
9+
Then, run the following from this directory to run the sample:
10+
11+
```bash
12+
poetry run python run_worker.py
13+
poetry run python run_periodic.py
14+
```
15+
16+
This will start a Workflow and Child Workflow to periodically poll an Activity.
17+
The Parent Workflow is not aware about the Child Workflow calling Continue-As-New, and it gets notified when it completes (or fails).

polling/periodic_sequence/__init__.py

Whitespace-only changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from dataclasses import dataclass
2+
3+
from temporalio import activity
4+
5+
6+
@dataclass
7+
class ComposeGreetingInput:
8+
greeting: str
9+
name: str
10+
11+
12+
@activity.defn
13+
async def compose_greeting(input: ComposeGreetingInput) -> str:
14+
raise RuntimeError("Service is down")
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import asyncio
2+
3+
from temporalio.client import Client
4+
from workflows import GreetingWorkflow
5+
6+
7+
async def main():
8+
client = await Client.connect("localhost:7233")
9+
result = await client.execute_workflow(
10+
GreetingWorkflow.run,
11+
"World",
12+
id="periodic-child-workflow-retry",
13+
task_queue="periodic-retry-task-queue",
14+
)
15+
print(f"Result: {result}")
16+
17+
18+
if __name__ == "__main__":
19+
asyncio.run(main())
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import asyncio
2+
3+
from activities import compose_greeting
4+
from temporalio.client import Client
5+
from temporalio.worker import Worker
6+
from workflows import ChildWorkflow, GreetingWorkflow
7+
8+
9+
async def main():
10+
client = await Client.connect("localhost:7233")
11+
12+
worker = Worker(
13+
client,
14+
task_queue="periodic-retry-task-queue",
15+
workflows=[GreetingWorkflow, ChildWorkflow],
16+
activities=[compose_greeting],
17+
)
18+
await worker.run()
19+
20+
21+
if __name__ == "__main__":
22+
asyncio.run(main())
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import asyncio
2+
from datetime import timedelta
3+
4+
from temporalio import workflow
5+
from temporalio.common import RetryPolicy
6+
from temporalio.exceptions import ActivityError
7+
8+
with workflow.unsafe.imports_passed_through():
9+
from activities import ComposeGreetingInput, compose_greeting
10+
11+
12+
@workflow.defn
13+
class GreetingWorkflow:
14+
@workflow.run
15+
async def run(self, name: str) -> str:
16+
return await workflow.execute_child_workflow(
17+
ChildWorkflow.run,
18+
name,
19+
)
20+
21+
22+
@workflow.defn
23+
class ChildWorkflow:
24+
@workflow.run
25+
async def run(self, name: str) -> str:
26+
for i in range(10):
27+
try:
28+
return await workflow.execute_activity(
29+
compose_greeting,
30+
ComposeGreetingInput("Hello", name),
31+
start_to_close_timeout=timedelta(seconds=4),
32+
retry_policy=RetryPolicy(
33+
maximum_attempts=1,
34+
),
35+
)
36+
37+
except ActivityError:
38+
workflow.logger.error("Activity failed, retrying in 1 seconds")
39+
await asyncio.sleep(1)
40+
workflow.continue_as_new(name)
41+
42+
raise Exception("Polling failed after all attempts")

polling/test_service.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
class TestService:
2+
def __init__(self):
3+
self.try_attempts = 0
4+
self.error_attempts = 5
5+
6+
def get_service_result(self, input):
7+
print(
8+
f"Attempt {self.try_attempts}"
9+
f" of {self.error_attempts} to invoke service"
10+
)
11+
self.try_attempts += 1
12+
if self.try_attempts % self.error_attempts == 0:
13+
return f"{input.greeting}, {input.name}!"
14+
raise Exception("service is down")

0 commit comments

Comments
 (0)