cadence-python-client 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cadence/__init__.py +18 -0
- cadence/_internal/__init__.py +8 -0
- cadence/_internal/activity/__init__.py +5 -0
- cadence/_internal/activity/_activity_executor.py +113 -0
- cadence/_internal/activity/_context.py +58 -0
- cadence/_internal/rpc/__init__.py +0 -0
- cadence/_internal/rpc/error.py +148 -0
- cadence/_internal/rpc/retry.py +104 -0
- cadence/_internal/rpc/yarpc.py +42 -0
- cadence/_internal/workflow/__init__.py +0 -0
- cadence/_internal/workflow/context.py +121 -0
- cadence/_internal/workflow/decision_events_iterator.py +161 -0
- cadence/_internal/workflow/decisions_helper.py +312 -0
- cadence/_internal/workflow/deterministic_event_loop.py +498 -0
- cadence/_internal/workflow/history_event_iterator.py +58 -0
- cadence/_internal/workflow/statemachine/__init__.py +0 -0
- cadence/_internal/workflow/statemachine/activity_state_machine.py +106 -0
- cadence/_internal/workflow/statemachine/decision_manager.py +157 -0
- cadence/_internal/workflow/statemachine/decision_state_machine.py +87 -0
- cadence/_internal/workflow/statemachine/event_dispatcher.py +76 -0
- cadence/_internal/workflow/statemachine/timer_state_machine.py +73 -0
- cadence/_internal/workflow/workflow_engine.py +245 -0
- cadence/_internal/workflow/workflow_intance.py +44 -0
- cadence/activity.py +255 -0
- cadence/api/v1/__init__.py +92 -0
- cadence/api/v1/common_pb2.py +90 -0
- cadence/api/v1/common_pb2.pyi +200 -0
- cadence/api/v1/common_pb2_grpc.py +24 -0
- cadence/api/v1/decision_pb2.py +67 -0
- cadence/api/v1/decision_pb2.pyi +225 -0
- cadence/api/v1/decision_pb2_grpc.py +24 -0
- cadence/api/v1/domain_pb2.py +68 -0
- cadence/api/v1/domain_pb2.pyi +145 -0
- cadence/api/v1/domain_pb2_grpc.py +24 -0
- cadence/api/v1/error_pb2.py +59 -0
- cadence/api/v1/error_pb2.pyi +82 -0
- cadence/api/v1/error_pb2_grpc.py +24 -0
- cadence/api/v1/history_pb2.py +134 -0
- cadence/api/v1/history_pb2.pyi +780 -0
- cadence/api/v1/history_pb2_grpc.py +24 -0
- cadence/api/v1/query_pb2.py +49 -0
- cadence/api/v1/query_pb2.pyi +59 -0
- cadence/api/v1/query_pb2_grpc.py +24 -0
- cadence/api/v1/service_domain_pb2.py +76 -0
- cadence/api/v1/service_domain_pb2.pyi +164 -0
- cadence/api/v1/service_domain_pb2_grpc.py +327 -0
- cadence/api/v1/service_meta_pb2.py +41 -0
- cadence/api/v1/service_meta_pb2.pyi +17 -0
- cadence/api/v1/service_meta_pb2_grpc.py +97 -0
- cadence/api/v1/service_visibility_pb2.py +71 -0
- cadence/api/v1/service_visibility_pb2.pyi +149 -0
- cadence/api/v1/service_visibility_pb2_grpc.py +362 -0
- cadence/api/v1/service_worker_pb2.py +116 -0
- cadence/api/v1/service_worker_pb2.pyi +350 -0
- cadence/api/v1/service_worker_pb2_grpc.py +743 -0
- cadence/api/v1/service_workflow_pb2.py +126 -0
- cadence/api/v1/service_workflow_pb2.pyi +395 -0
- cadence/api/v1/service_workflow_pb2_grpc.py +861 -0
- cadence/api/v1/tasklist_pb2.py +78 -0
- cadence/api/v1/tasklist_pb2.pyi +147 -0
- cadence/api/v1/tasklist_pb2_grpc.py +24 -0
- cadence/api/v1/visibility_pb2.py +47 -0
- cadence/api/v1/visibility_pb2.pyi +53 -0
- cadence/api/v1/visibility_pb2_grpc.py +24 -0
- cadence/api/v1/workflow_pb2.py +89 -0
- cadence/api/v1/workflow_pb2.pyi +365 -0
- cadence/api/v1/workflow_pb2_grpc.py +24 -0
- cadence/client.py +382 -0
- cadence/data_converter.py +78 -0
- cadence/error.py +111 -0
- cadence/metrics/__init__.py +12 -0
- cadence/metrics/constants.py +136 -0
- cadence/metrics/metrics.py +56 -0
- cadence/metrics/prometheus.py +165 -0
- cadence/sample/__init__.py +1 -0
- cadence/sample/client_example.py +15 -0
- cadence/sample/grpc_usage_example.py +230 -0
- cadence/sample/simple_usage_example.py +155 -0
- cadence/signal.py +174 -0
- cadence/worker/__init__.py +13 -0
- cadence/worker/_activity.py +60 -0
- cadence/worker/_base_task_handler.py +71 -0
- cadence/worker/_decision.py +62 -0
- cadence/worker/_decision_task_handler.py +285 -0
- cadence/worker/_poller.py +64 -0
- cadence/worker/_registry.py +245 -0
- cadence/worker/_types.py +26 -0
- cadence/worker/_worker.py +56 -0
- cadence/workflow.py +271 -0
- cadence_python_client-0.1.0.dist-info/METADATA +180 -0
- cadence_python_client-0.1.0.dist-info/RECORD +95 -0
- cadence_python_client-0.1.0.dist-info/WHEEL +5 -0
- cadence_python_client-0.1.0.dist-info/licenses/LICENSE +201 -0
- cadence_python_client-0.1.0.dist-info/licenses/NOTICE +19 -0
- cadence_python_client-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from cadence._internal.workflow.history_event_iterator import iterate_history_events
|
|
7
|
+
from cadence.api.v1.common_pb2 import Payload
|
|
8
|
+
from cadence.api.v1.service_worker_pb2 import (
|
|
9
|
+
PollForDecisionTaskResponse,
|
|
10
|
+
RespondDecisionTaskCompletedRequest,
|
|
11
|
+
RespondDecisionTaskFailedRequest,
|
|
12
|
+
)
|
|
13
|
+
from cadence.api.v1.workflow_pb2 import DecisionTaskFailedCause
|
|
14
|
+
from cadence.client import Client
|
|
15
|
+
from cadence.worker._base_task_handler import BaseTaskHandler
|
|
16
|
+
from cadence._internal.workflow.workflow_engine import WorkflowEngine, DecisionResult
|
|
17
|
+
from cadence.workflow import WorkflowInfo
|
|
18
|
+
from cadence.worker._registry import Registry
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DecisionTaskHandler(BaseTaskHandler[PollForDecisionTaskResponse]):
|
|
24
|
+
"""
|
|
25
|
+
Task handler for processing decision tasks.
|
|
26
|
+
|
|
27
|
+
This handler processes decision tasks and generates decisions using workflow engines.
|
|
28
|
+
Uses a thread-safe cache to hold workflow engines for concurrent decision task handling.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
client: Client,
|
|
34
|
+
task_list: str,
|
|
35
|
+
registry: Registry,
|
|
36
|
+
identity: str = "unknown",
|
|
37
|
+
executor: Optional[ThreadPoolExecutor] = None,
|
|
38
|
+
**options,
|
|
39
|
+
):
|
|
40
|
+
"""
|
|
41
|
+
Initialize the decision task handler.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
client: The Cadence client instance
|
|
45
|
+
task_list: The task list name
|
|
46
|
+
registry: Registry containing workflow functions
|
|
47
|
+
identity: The worker identity
|
|
48
|
+
**options: Additional options for the handler
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(client, task_list, identity, **options)
|
|
51
|
+
self._registry = registry
|
|
52
|
+
self._executor = executor
|
|
53
|
+
|
|
54
|
+
async def _handle_task_implementation(
|
|
55
|
+
self, task: PollForDecisionTaskResponse
|
|
56
|
+
) -> None:
|
|
57
|
+
"""
|
|
58
|
+
Handle a decision task implementation.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
task: The decision task to handle
|
|
62
|
+
"""
|
|
63
|
+
# Extract workflow execution info
|
|
64
|
+
workflow_execution = task.workflow_execution
|
|
65
|
+
workflow_type = task.workflow_type
|
|
66
|
+
|
|
67
|
+
if not workflow_execution or not workflow_type:
|
|
68
|
+
logger.error(
|
|
69
|
+
"Decision task missing workflow execution or type. Task: %r", task
|
|
70
|
+
)
|
|
71
|
+
raise ValueError("Missing workflow execution or type")
|
|
72
|
+
|
|
73
|
+
workflow_id = workflow_execution.workflow_id
|
|
74
|
+
run_id = workflow_execution.run_id
|
|
75
|
+
workflow_type_name = workflow_type.name
|
|
76
|
+
|
|
77
|
+
# This log matches the WorkflowEngine but at task handler level (like Java ReplayDecisionTaskHandler)
|
|
78
|
+
logger.info(
|
|
79
|
+
"Received decision task for workflow",
|
|
80
|
+
extra={
|
|
81
|
+
"workflow_type": workflow_type_name,
|
|
82
|
+
"workflow_id": workflow_id,
|
|
83
|
+
"run_id": run_id,
|
|
84
|
+
"started_event_id": task.started_event_id,
|
|
85
|
+
"attempt": task.attempt,
|
|
86
|
+
"task_token": task.task_token[:16].hex()
|
|
87
|
+
if task.task_token
|
|
88
|
+
else None, # Log partial token for debugging
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
workflow_definition = self._registry.get_workflow(workflow_type_name)
|
|
94
|
+
except KeyError:
|
|
95
|
+
logger.error(
|
|
96
|
+
"Workflow type not found in registry",
|
|
97
|
+
extra={
|
|
98
|
+
"workflow_type": workflow_type_name,
|
|
99
|
+
"workflow_id": workflow_id,
|
|
100
|
+
"run_id": run_id,
|
|
101
|
+
"error_type": "workflow_not_registered",
|
|
102
|
+
},
|
|
103
|
+
)
|
|
104
|
+
raise KeyError(f"Workflow type '{workflow_type_name}' not found")
|
|
105
|
+
|
|
106
|
+
# fetch full workflow history
|
|
107
|
+
# TODO sticky cache
|
|
108
|
+
workflow_events = [
|
|
109
|
+
event async for event in iterate_history_events(task, self._client)
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
# Create workflow info and get or create workflow engine from cache
|
|
113
|
+
workflow_info = WorkflowInfo(
|
|
114
|
+
workflow_type=workflow_type_name,
|
|
115
|
+
workflow_domain=self._client.domain,
|
|
116
|
+
workflow_id=workflow_id,
|
|
117
|
+
workflow_run_id=run_id,
|
|
118
|
+
workflow_task_list=self.task_list,
|
|
119
|
+
data_converter=self._client.data_converter,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
workflow_engine = WorkflowEngine(
|
|
123
|
+
info=workflow_info,
|
|
124
|
+
workflow_definition=workflow_definition,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
decision_result = await asyncio.get_running_loop().run_in_executor(
|
|
128
|
+
self._executor, workflow_engine.process_decision, workflow_events
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
# Respond with the decisions
|
|
132
|
+
await self._respond_decision_task_completed(task, decision_result)
|
|
133
|
+
|
|
134
|
+
logger.info(
|
|
135
|
+
"Successfully processed decision task",
|
|
136
|
+
extra={
|
|
137
|
+
"workflow_type": workflow_type_name,
|
|
138
|
+
"workflow_id": workflow_id,
|
|
139
|
+
"run_id": run_id,
|
|
140
|
+
"started_event_id": task.started_event_id,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
async def handle_task_failure(
|
|
145
|
+
self, task: PollForDecisionTaskResponse, error: Exception
|
|
146
|
+
) -> None:
|
|
147
|
+
"""
|
|
148
|
+
Handle decision task processing failure.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
task: The task that failed
|
|
152
|
+
error: The exception that occurred
|
|
153
|
+
"""
|
|
154
|
+
# Extract workflow context for error logging (matches Java ReplayDecisionTaskHandler error patterns)
|
|
155
|
+
workflow_execution = task.workflow_execution
|
|
156
|
+
workflow_id = (
|
|
157
|
+
workflow_execution.workflow_id if workflow_execution else "unknown"
|
|
158
|
+
)
|
|
159
|
+
run_id = workflow_execution.run_id if workflow_execution else "unknown"
|
|
160
|
+
workflow_type = task.workflow_type.name if task.workflow_type else "unknown"
|
|
161
|
+
|
|
162
|
+
# Log task failure with full context (matches Java error logging)
|
|
163
|
+
logger.error(
|
|
164
|
+
"Decision task processing failure",
|
|
165
|
+
extra={
|
|
166
|
+
"workflow_type": workflow_type,
|
|
167
|
+
"workflow_id": workflow_id,
|
|
168
|
+
"run_id": run_id,
|
|
169
|
+
"started_event_id": task.started_event_id,
|
|
170
|
+
"attempt": task.attempt,
|
|
171
|
+
"error_type": type(error).__name__,
|
|
172
|
+
"error_message": str(error),
|
|
173
|
+
},
|
|
174
|
+
exc_info=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
# Determine the failure cause
|
|
178
|
+
cause = DecisionTaskFailedCause.DECISION_TASK_FAILED_CAUSE_UNHANDLED_DECISION
|
|
179
|
+
if isinstance(error, KeyError):
|
|
180
|
+
cause = DecisionTaskFailedCause.DECISION_TASK_FAILED_CAUSE_WORKFLOW_WORKER_UNHANDLED_FAILURE
|
|
181
|
+
elif isinstance(error, ValueError):
|
|
182
|
+
cause = DecisionTaskFailedCause.DECISION_TASK_FAILED_CAUSE_BAD_SCHEDULE_ACTIVITY_ATTRIBUTES
|
|
183
|
+
|
|
184
|
+
# Create error details
|
|
185
|
+
# TODO: Use a data converter for error details serialization
|
|
186
|
+
error_message = str(error).encode("utf-8")
|
|
187
|
+
details = Payload(data=error_message)
|
|
188
|
+
|
|
189
|
+
# Respond with failure
|
|
190
|
+
try:
|
|
191
|
+
await self._client.worker_stub.RespondDecisionTaskFailed(
|
|
192
|
+
RespondDecisionTaskFailedRequest(
|
|
193
|
+
task_token=task.task_token,
|
|
194
|
+
cause=cause,
|
|
195
|
+
identity=self._identity,
|
|
196
|
+
details=details,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
logger.info(
|
|
200
|
+
"Decision task failure response sent",
|
|
201
|
+
extra={
|
|
202
|
+
"workflow_id": workflow_id,
|
|
203
|
+
"run_id": run_id,
|
|
204
|
+
"cause": cause,
|
|
205
|
+
"task_token": task.task_token[:16].hex()
|
|
206
|
+
if task.task_token
|
|
207
|
+
else None,
|
|
208
|
+
},
|
|
209
|
+
)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.error(
|
|
212
|
+
"Error responding to decision task failure",
|
|
213
|
+
extra={
|
|
214
|
+
"workflow_id": workflow_id,
|
|
215
|
+
"run_id": run_id,
|
|
216
|
+
"original_error": type(error).__name__,
|
|
217
|
+
"response_error": type(e).__name__,
|
|
218
|
+
},
|
|
219
|
+
exc_info=True,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
async def _respond_decision_task_completed(
|
|
223
|
+
self, task: PollForDecisionTaskResponse, decision_result: DecisionResult
|
|
224
|
+
) -> None:
|
|
225
|
+
"""
|
|
226
|
+
Respond to the service that the decision task has been completed.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
task: The original decision task
|
|
230
|
+
decision_result: The result containing decisions and query results
|
|
231
|
+
"""
|
|
232
|
+
try:
|
|
233
|
+
request = RespondDecisionTaskCompletedRequest(
|
|
234
|
+
task_token=task.task_token,
|
|
235
|
+
decisions=decision_result.decisions,
|
|
236
|
+
identity=self._identity,
|
|
237
|
+
return_new_decision_task=True,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
await self._client.worker_stub.RespondDecisionTaskCompleted(request)
|
|
241
|
+
|
|
242
|
+
# Log completion response (matches Java ReplayDecisionTaskHandler trace/debug patterns)
|
|
243
|
+
workflow_execution = task.workflow_execution
|
|
244
|
+
logger.debug(
|
|
245
|
+
"Decision task completion response sent",
|
|
246
|
+
extra={
|
|
247
|
+
"workflow_type": task.workflow_type.name
|
|
248
|
+
if task.workflow_type
|
|
249
|
+
else "unknown",
|
|
250
|
+
"workflow_id": workflow_execution.workflow_id
|
|
251
|
+
if workflow_execution
|
|
252
|
+
else "unknown",
|
|
253
|
+
"run_id": workflow_execution.run_id
|
|
254
|
+
if workflow_execution
|
|
255
|
+
else "unknown",
|
|
256
|
+
"started_event_id": task.started_event_id,
|
|
257
|
+
"decisions_count": len(decision_result.decisions),
|
|
258
|
+
"return_new_decision_task": True,
|
|
259
|
+
"task_token": task.task_token[:16].hex()
|
|
260
|
+
if task.task_token
|
|
261
|
+
else None,
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
except Exception as e:
|
|
266
|
+
workflow_execution = task.workflow_execution
|
|
267
|
+
logger.error(
|
|
268
|
+
"Error responding to decision task completion",
|
|
269
|
+
extra={
|
|
270
|
+
"workflow_type": task.workflow_type.name
|
|
271
|
+
if task.workflow_type
|
|
272
|
+
else "unknown",
|
|
273
|
+
"workflow_id": workflow_execution.workflow_id
|
|
274
|
+
if workflow_execution
|
|
275
|
+
else "unknown",
|
|
276
|
+
"run_id": workflow_execution.run_id
|
|
277
|
+
if workflow_execution
|
|
278
|
+
else "unknown",
|
|
279
|
+
"started_event_id": task.started_event_id,
|
|
280
|
+
"decisions_count": len(decision_result.decisions),
|
|
281
|
+
"error_type": type(e).__name__,
|
|
282
|
+
},
|
|
283
|
+
exc_info=True,
|
|
284
|
+
)
|
|
285
|
+
raise
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Callable, TypeVar, Generic, Awaitable, Optional
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
T = TypeVar("T")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Poller(Generic[T]):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
num_tasks: int,
|
|
14
|
+
permits: asyncio.Semaphore,
|
|
15
|
+
poll: Callable[[], Awaitable[Optional[T]]],
|
|
16
|
+
callback: Callable[[T], Awaitable[None]],
|
|
17
|
+
) -> None:
|
|
18
|
+
self._num_tasks = num_tasks
|
|
19
|
+
self._permits = permits
|
|
20
|
+
self._poll = poll
|
|
21
|
+
self._callback = callback
|
|
22
|
+
self._background_tasks: set[asyncio.Task[None]] = set()
|
|
23
|
+
|
|
24
|
+
async def run(self) -> None:
|
|
25
|
+
try:
|
|
26
|
+
async with asyncio.TaskGroup() as tg:
|
|
27
|
+
for i in range(self._num_tasks):
|
|
28
|
+
tg.create_task(self._poll_loop())
|
|
29
|
+
except asyncio.CancelledError:
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
async def _poll_loop(self) -> None:
|
|
33
|
+
while True:
|
|
34
|
+
try:
|
|
35
|
+
await self._poll_and_dispatch()
|
|
36
|
+
except asyncio.CancelledError as e:
|
|
37
|
+
raise e
|
|
38
|
+
except Exception:
|
|
39
|
+
logger.exception("Exception while polling")
|
|
40
|
+
|
|
41
|
+
async def _poll_and_dispatch(self) -> None:
|
|
42
|
+
await self._permits.acquire()
|
|
43
|
+
try:
|
|
44
|
+
task = await self._poll()
|
|
45
|
+
except Exception as e:
|
|
46
|
+
self._permits.release()
|
|
47
|
+
raise e
|
|
48
|
+
|
|
49
|
+
if task is None:
|
|
50
|
+
self._permits.release()
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
# Need to store a reference to the async task or it may be garbage collected
|
|
54
|
+
scheduled = asyncio.create_task(self._execute_callback(task))
|
|
55
|
+
self._background_tasks.add(scheduled)
|
|
56
|
+
scheduled.add_done_callback(self._background_tasks.remove)
|
|
57
|
+
|
|
58
|
+
async def _execute_callback(self, task: T) -> None:
|
|
59
|
+
try:
|
|
60
|
+
await self._callback(task)
|
|
61
|
+
except Exception:
|
|
62
|
+
logger.exception("Exception during callback")
|
|
63
|
+
finally:
|
|
64
|
+
self._permits.release()
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Workflow and Activity Registry for Cadence Python Client.
|
|
4
|
+
|
|
5
|
+
This module provides a registry system for managing workflows and activities,
|
|
6
|
+
similar to the Go client's registry.go implementation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import (
|
|
11
|
+
Callable,
|
|
12
|
+
Dict,
|
|
13
|
+
Optional,
|
|
14
|
+
Unpack,
|
|
15
|
+
TypedDict,
|
|
16
|
+
overload,
|
|
17
|
+
Type,
|
|
18
|
+
Union,
|
|
19
|
+
TypeVar,
|
|
20
|
+
)
|
|
21
|
+
from cadence.activity import (
|
|
22
|
+
ActivityDefinitionOptions,
|
|
23
|
+
ActivityDefinition,
|
|
24
|
+
ActivityDecorator,
|
|
25
|
+
P,
|
|
26
|
+
T,
|
|
27
|
+
)
|
|
28
|
+
from cadence.workflow import WorkflowDefinition, WorkflowDefinitionOptions
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
# TypeVar for workflow class types
|
|
33
|
+
W = TypeVar("W")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RegisterWorkflowOptions(TypedDict, total=False):
|
|
37
|
+
"""Options for registering a workflow."""
|
|
38
|
+
|
|
39
|
+
name: Optional[str]
|
|
40
|
+
alias: Optional[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Registry:
|
|
44
|
+
"""
|
|
45
|
+
Registry for managing workflows and activities.
|
|
46
|
+
|
|
47
|
+
This class provides functionality to register, retrieve, and manage
|
|
48
|
+
workflows and activities in a Cadence application.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
"""Initialize the registry."""
|
|
53
|
+
self._workflows: Dict[str, WorkflowDefinition] = {}
|
|
54
|
+
self._activities: Dict[str, ActivityDefinition] = {}
|
|
55
|
+
self._workflow_aliases: Dict[str, str] = {} # alias -> name mapping
|
|
56
|
+
|
|
57
|
+
def workflow(
|
|
58
|
+
self, cls: Optional[Type[W]] = None, **kwargs: Unpack[RegisterWorkflowOptions]
|
|
59
|
+
) -> Union[Type[W], Callable[[Type[W]], Type[W]]]:
|
|
60
|
+
"""
|
|
61
|
+
Register a workflow class.
|
|
62
|
+
|
|
63
|
+
This method can be used as a decorator or called directly.
|
|
64
|
+
Only supports class-based workflows.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
cls: The workflow class to register
|
|
68
|
+
**kwargs: Options for registration (name, alias)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The decorated class
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
KeyError: If workflow name already exists
|
|
75
|
+
ValueError: If class workflow is invalid
|
|
76
|
+
"""
|
|
77
|
+
options = RegisterWorkflowOptions(**kwargs)
|
|
78
|
+
|
|
79
|
+
def decorator(target: Type[W]) -> Type[W]:
|
|
80
|
+
workflow_name = options.get("name") or target.__name__
|
|
81
|
+
|
|
82
|
+
if workflow_name in self._workflows:
|
|
83
|
+
raise KeyError(f"Workflow '{workflow_name}' is already registered")
|
|
84
|
+
|
|
85
|
+
# Create WorkflowDefinition with type information
|
|
86
|
+
workflow_opts = WorkflowDefinitionOptions(name=workflow_name)
|
|
87
|
+
workflow_def = WorkflowDefinition.wrap(target, workflow_opts)
|
|
88
|
+
self._workflows[workflow_name] = workflow_def
|
|
89
|
+
|
|
90
|
+
# Register alias if provided
|
|
91
|
+
alias = options.get("alias")
|
|
92
|
+
if alias:
|
|
93
|
+
if alias in self._workflow_aliases:
|
|
94
|
+
raise KeyError(f"Workflow alias '{alias}' is already registered")
|
|
95
|
+
self._workflow_aliases[alias] = workflow_name
|
|
96
|
+
|
|
97
|
+
logger.info(f"Registered workflow '{workflow_name}'")
|
|
98
|
+
return target
|
|
99
|
+
|
|
100
|
+
if cls is None:
|
|
101
|
+
return decorator
|
|
102
|
+
return decorator(cls)
|
|
103
|
+
|
|
104
|
+
@overload
|
|
105
|
+
def activity(self, func: Callable[P, T]) -> ActivityDefinition[P, T]: ...
|
|
106
|
+
|
|
107
|
+
@overload
|
|
108
|
+
def activity(
|
|
109
|
+
self, **kwargs: Unpack[ActivityDefinitionOptions]
|
|
110
|
+
) -> ActivityDecorator: ...
|
|
111
|
+
|
|
112
|
+
def activity(
|
|
113
|
+
self,
|
|
114
|
+
func: Callable[P, T] | None = None,
|
|
115
|
+
**kwargs: Unpack[ActivityDefinitionOptions],
|
|
116
|
+
) -> ActivityDecorator | ActivityDefinition[P, T]:
|
|
117
|
+
"""
|
|
118
|
+
Register an activity function.
|
|
119
|
+
|
|
120
|
+
This method can be used as a decorator or called directly.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
func: The activity function to register
|
|
124
|
+
**kwargs: Options for registration (name, alias)
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
The decorated function or the function itself
|
|
128
|
+
|
|
129
|
+
Raises:
|
|
130
|
+
KeyError: If activity name already exists
|
|
131
|
+
"""
|
|
132
|
+
options = ActivityDefinitionOptions(**kwargs)
|
|
133
|
+
|
|
134
|
+
def decorator(f: Callable[P, T]) -> ActivityDefinition[P, T]:
|
|
135
|
+
defn = ActivityDefinition.wrap(f, options)
|
|
136
|
+
|
|
137
|
+
self._register_activity(defn)
|
|
138
|
+
|
|
139
|
+
return defn
|
|
140
|
+
|
|
141
|
+
if func is not None:
|
|
142
|
+
return decorator(func)
|
|
143
|
+
|
|
144
|
+
return decorator
|
|
145
|
+
|
|
146
|
+
def register_activities(self, obj: object) -> None:
|
|
147
|
+
activities = _find_activity_definitions(obj)
|
|
148
|
+
if not activities:
|
|
149
|
+
raise ValueError(f"No activity definitions found in '{repr(obj)}'")
|
|
150
|
+
|
|
151
|
+
for defn in activities:
|
|
152
|
+
self._register_activity(defn)
|
|
153
|
+
|
|
154
|
+
def register_activity(self, defn: Callable) -> None:
|
|
155
|
+
if not isinstance(defn, ActivityDefinition):
|
|
156
|
+
raise ValueError(f"{defn.__qualname__} must have @activity.defn decorator")
|
|
157
|
+
self._register_activity(defn)
|
|
158
|
+
|
|
159
|
+
def _register_activity(self, defn: ActivityDefinition) -> None:
|
|
160
|
+
if defn.name in self._activities:
|
|
161
|
+
raise KeyError(f"Activity '{defn.name}' is already registered")
|
|
162
|
+
|
|
163
|
+
self._activities[defn.name] = defn
|
|
164
|
+
|
|
165
|
+
def get_workflow(self, name: str) -> WorkflowDefinition:
|
|
166
|
+
"""
|
|
167
|
+
Get a registered workflow by name.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
name: Name or alias of the workflow
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
The workflow definition
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
KeyError: If workflow is not found
|
|
177
|
+
"""
|
|
178
|
+
# Check if it's an alias
|
|
179
|
+
actual_name = self._workflow_aliases.get(name, name)
|
|
180
|
+
|
|
181
|
+
if actual_name not in self._workflows:
|
|
182
|
+
raise KeyError(f"Workflow '{name}' not found in registry")
|
|
183
|
+
|
|
184
|
+
return self._workflows[actual_name]
|
|
185
|
+
|
|
186
|
+
def get_activity(self, name: str) -> ActivityDefinition:
|
|
187
|
+
"""
|
|
188
|
+
Get a registered activity by name.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
name: Name or alias of the activity
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
The activity function
|
|
195
|
+
|
|
196
|
+
Raises:
|
|
197
|
+
KeyError: If activity is not found
|
|
198
|
+
"""
|
|
199
|
+
return self._activities[name]
|
|
200
|
+
|
|
201
|
+
def __add__(self, other: "Registry") -> "Registry":
|
|
202
|
+
result = Registry()
|
|
203
|
+
for name, fn in self._activities.items():
|
|
204
|
+
result._register_activity(fn)
|
|
205
|
+
for name, fn in other._activities.items():
|
|
206
|
+
result._register_activity(fn)
|
|
207
|
+
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def of(*args: "Registry") -> "Registry":
|
|
212
|
+
result = Registry()
|
|
213
|
+
for other in args:
|
|
214
|
+
result += other
|
|
215
|
+
|
|
216
|
+
return result
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _find_activity_definitions(instance: object) -> list[ActivityDefinition]:
|
|
220
|
+
attr_to_def: dict[str, ActivityDefinition] = {}
|
|
221
|
+
for t in instance.__class__.__mro__:
|
|
222
|
+
for attr in dir(t):
|
|
223
|
+
if attr.startswith("_"):
|
|
224
|
+
continue
|
|
225
|
+
value = getattr(t, attr)
|
|
226
|
+
if isinstance(value, ActivityDefinition):
|
|
227
|
+
if attr in attr_to_def:
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"'{attr}' was overridden with a duplicate activity definition"
|
|
230
|
+
)
|
|
231
|
+
attr_to_def[attr] = value
|
|
232
|
+
|
|
233
|
+
result: list[ActivityDefinition] = []
|
|
234
|
+
for attr, definition in attr_to_def.items():
|
|
235
|
+
result.append(
|
|
236
|
+
ActivityDefinition(
|
|
237
|
+
getattr(instance, attr),
|
|
238
|
+
definition.name,
|
|
239
|
+
definition.strategy,
|
|
240
|
+
definition.params,
|
|
241
|
+
definition.result_type,
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
return result
|
cadence/worker/_types.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class WorkerOptions(TypedDict, total=False):
|
|
5
|
+
max_concurrent_activity_execution_size: int
|
|
6
|
+
max_concurrent_decision_task_execution_size: int
|
|
7
|
+
task_list_activities_per_second: float
|
|
8
|
+
# Remove these in favor of introducing automatic scaling prior to release
|
|
9
|
+
activity_task_pollers: int
|
|
10
|
+
decision_task_pollers: int
|
|
11
|
+
disable_workflow_worker: bool
|
|
12
|
+
disable_activity_worker: bool
|
|
13
|
+
identity: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_DEFAULT_WORKER_OPTIONS: WorkerOptions = {
|
|
17
|
+
"max_concurrent_activity_execution_size": 1000,
|
|
18
|
+
"max_concurrent_decision_task_execution_size": 1000,
|
|
19
|
+
"task_list_activities_per_second": 0.0,
|
|
20
|
+
"activity_task_pollers": 2,
|
|
21
|
+
"decision_task_pollers": 2,
|
|
22
|
+
"disable_workflow_worker": False,
|
|
23
|
+
"disable_activity_worker": False,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
_LONG_POLL_TIMEOUT = 60.0
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import uuid
|
|
3
|
+
from typing import Unpack, cast
|
|
4
|
+
|
|
5
|
+
from cadence.client import Client
|
|
6
|
+
from cadence.worker._registry import Registry
|
|
7
|
+
from cadence.worker._activity import ActivityWorker
|
|
8
|
+
from cadence.worker._decision import DecisionWorker
|
|
9
|
+
from cadence.worker._types import WorkerOptions, _DEFAULT_WORKER_OPTIONS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Worker:
|
|
13
|
+
def __init__(
|
|
14
|
+
self,
|
|
15
|
+
client: Client,
|
|
16
|
+
task_list: str,
|
|
17
|
+
registry: Registry,
|
|
18
|
+
**kwargs: Unpack[WorkerOptions],
|
|
19
|
+
) -> None:
|
|
20
|
+
self._client = client
|
|
21
|
+
self._task_list = task_list
|
|
22
|
+
|
|
23
|
+
options = WorkerOptions(**kwargs)
|
|
24
|
+
_validate_and_copy_defaults(client, task_list, options)
|
|
25
|
+
self._options = options
|
|
26
|
+
self._activity_worker = ActivityWorker(client, task_list, registry, options)
|
|
27
|
+
self._decision_worker = DecisionWorker(client, task_list, registry, options)
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def client(self) -> Client:
|
|
31
|
+
return self._client
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def task_list(self) -> str:
|
|
35
|
+
return self._task_list
|
|
36
|
+
|
|
37
|
+
async def run(self) -> None:
|
|
38
|
+
async with asyncio.TaskGroup() as tg:
|
|
39
|
+
if not self._options["disable_workflow_worker"]:
|
|
40
|
+
tg.create_task(self._decision_worker.run())
|
|
41
|
+
if not self._options["disable_activity_worker"]:
|
|
42
|
+
tg.create_task(self._activity_worker.run())
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _validate_and_copy_defaults(
|
|
46
|
+
client: Client, task_list: str, options: WorkerOptions
|
|
47
|
+
) -> None:
|
|
48
|
+
if "identity" not in options:
|
|
49
|
+
options["identity"] = f"{client.identity}@{task_list}@{uuid.uuid4()}"
|
|
50
|
+
|
|
51
|
+
# TODO: More validation
|
|
52
|
+
|
|
53
|
+
# Set default values for missing options
|
|
54
|
+
for key, value in _DEFAULT_WORKER_OPTIONS.items():
|
|
55
|
+
if key not in options:
|
|
56
|
+
cast(dict, options)[key] = value
|