Repository navigation
Expand file tree
/
Copy paththreaded_heartbeats.py
More file actions
56 lines (47 loc) · 1.84 KB
/
Copy paththreaded_heartbeats.py
File metadata and controls
56 lines (47 loc) · 1.84 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
"""Heartbeat a bounded, cooperatively cancellable call from a sync activity."""
from __future__ import annotations
import asyncio
from collections.abc import Callable
from queue import SimpleQueue
from threading import Event, Thread
from typing import TypeVar
from durable_workflow import activity
from durable_workflow.errors import ActivityCancelled
T = TypeVar("T")
def call_with_heartbeats(work: Callable[[Event], T], *, interval: float = 5.0) -> T:
"""Run work(cancelled), which must enforce its own finite deadline.
Work must return or raise promptly when cancelled is set. This helper cannot
interrupt an arbitrary blocking call. Call it from a synchronous activity,
never from the worker event loop.
"""
try:
asyncio.get_running_loop()
except RuntimeError:
pass
else:
raise RuntimeError("call_with_heartbeats requires a synchronous activity thread")
if not 0 < interval < float("inf"):
raise ValueError("interval must be finite and positive")
ctx = activity.context()
finished = Event()
cancelled = Event()
failures: SimpleQueue[BaseException] = SimpleQueue()
def heartbeat() -> None:
while not finished.wait(interval):
try:
ctx.heartbeat_sync({"message": "Waiting for remote operation"})
except (ActivityCancelled, Exception) as error:
# ActivityCancelled inherits BaseException, not Exception.
failures.put(error)
cancelled.set()
return
helper = Thread(target=heartbeat, name="activity-heartbeat")
helper.start()
try:
return work(cancelled)
finally:
finished.set()
# Keep the captured context and client alive until the helper exits.
helper.join()
if not failures.empty():
raise failures.get()