cadence-python-client 0.2.2__py3-none-any.whl → 0.3.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/_internal/rpc/error.py +24 -24
- cadence/_internal/rpc/retry.py +17 -13
- cadence/_internal/rpc/yarpc.py +7 -8
- cadence/_internal/workflow/context.py +158 -1
- cadence/_internal/workflow/memo.py +40 -0
- cadence/_internal/workflow/statemachine/child_workflow_execution_state_machine.py +184 -0
- cadence/_internal/workflow/statemachine/decision_manager.py +78 -13
- cadence/_internal/workflow/statemachine/event_dispatcher.py +30 -5
- cadence/_internal/workflow/statemachine/nondeterminism.py +88 -0
- cadence/_internal/workflow/statemachine/signal_external_workflow_state_machine.py +74 -0
- cadence/_internal/workflow/workflow_engine.py +26 -2
- cadence/_internal/workflow/workflow_instance.py +33 -0
- cadence/api/v1/common_pb2.py +13 -31
- cadence/api/v1/common_pb2.pyi +2 -31
- cadence/api/v1/schedule_pb2.py +8 -8
- cadence/api/v1/schedule_pb2.pyi +6 -2
- cadence/api/v1/service_domain_pb2.py +32 -32
- cadence/api/v1/service_domain_pb2.pyi +4 -2
- cadence/api/v1/tasklist_pb2.py +30 -30
- cadence/api/v1/tasklist_pb2.pyi +4 -2
- cadence/client.py +317 -3
- cadence/contrib/openai/cadence_tool.py +6 -0
- cadence/error.py +39 -0
- cadence/query.py +103 -0
- cadence/worker/_decision_task_handler.py +114 -15
- cadence/workflow.py +227 -8
- {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/METADATA +54 -61
- {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/RECORD +32 -28
- {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/WHEEL +0 -0
- {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/licenses/NOTICE +0 -0
- {cadence_python_client-0.2.2.dist-info → cadence_python_client-0.3.0.dist-info}/top_level.txt +0 -0
cadence/_internal/rpc/error.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
from typing import Callable, Any, Optional, Generator, TypeVar
|
|
1
|
+
from typing import Callable, Any, Optional, Generator, TypeVar, cast
|
|
2
2
|
|
|
3
3
|
import grpc
|
|
4
|
-
from google.rpc.status_pb2 import Status
|
|
4
|
+
from google.rpc.status_pb2 import Status
|
|
5
5
|
from grpc.aio import (
|
|
6
6
|
UnaryUnaryClientInterceptor,
|
|
7
7
|
ClientCallDetails,
|
|
@@ -9,7 +9,7 @@ from grpc.aio import (
|
|
|
9
9
|
UnaryUnaryCall,
|
|
10
10
|
Metadata,
|
|
11
11
|
)
|
|
12
|
-
from grpc_status.rpc_status import from_call
|
|
12
|
+
from grpc_status.rpc_status import from_call
|
|
13
13
|
|
|
14
14
|
from cadence.api.v1 import error_pb2
|
|
15
15
|
from cadence import error
|
|
@@ -46,22 +46,22 @@ class CadenceErrorUnaryUnaryCall(UnaryUnaryCall[RequestType, ResponseType]):
|
|
|
46
46
|
return await self._wrapped.code()
|
|
47
47
|
|
|
48
48
|
async def details(self) -> str:
|
|
49
|
-
return await self._wrapped.details()
|
|
49
|
+
return await self._wrapped.details()
|
|
50
50
|
|
|
51
51
|
async def wait_for_connection(self) -> None:
|
|
52
52
|
await self._wrapped.wait_for_connection()
|
|
53
53
|
|
|
54
54
|
def cancelled(self) -> bool:
|
|
55
|
-
return self._wrapped.cancelled()
|
|
55
|
+
return self._wrapped.cancelled()
|
|
56
56
|
|
|
57
57
|
def done(self) -> bool:
|
|
58
|
-
return self._wrapped.done()
|
|
58
|
+
return self._wrapped.done()
|
|
59
59
|
|
|
60
60
|
def time_remaining(self) -> Optional[float]:
|
|
61
|
-
return self._wrapped.time_remaining()
|
|
61
|
+
return self._wrapped.time_remaining()
|
|
62
62
|
|
|
63
63
|
def cancel(self) -> bool:
|
|
64
|
-
return self._wrapped.cancel()
|
|
64
|
+
return self._wrapped.cancel()
|
|
65
65
|
|
|
66
66
|
def add_done_callback(self, callback: DoneCallbackType) -> None:
|
|
67
67
|
self._wrapped.add_done_callback(callback)
|
|
@@ -79,16 +79,18 @@ class CadenceErrorInterceptor(UnaryUnaryClientInterceptor):
|
|
|
79
79
|
|
|
80
80
|
|
|
81
81
|
def map_error(e: AioRpcError) -> error.CadenceRpcError:
|
|
82
|
-
|
|
82
|
+
# AioRpcError implements the grpc.Call interface but doesn't inherit from it in the type stubs
|
|
83
|
+
status: Status | None = from_call(cast(grpc.Call, e))
|
|
84
|
+
message = e.details() or ""
|
|
83
85
|
if not status or not status.details:
|
|
84
|
-
return error.CadenceRpcError(
|
|
86
|
+
return error.CadenceRpcError(message, e.code())
|
|
85
87
|
|
|
86
88
|
details = status.details[0]
|
|
87
89
|
if details.Is(error_pb2.WorkflowExecutionAlreadyStartedError.DESCRIPTOR):
|
|
88
90
|
already_started = error_pb2.WorkflowExecutionAlreadyStartedError()
|
|
89
91
|
details.Unpack(already_started)
|
|
90
92
|
return error.WorkflowExecutionAlreadyStartedError(
|
|
91
|
-
|
|
93
|
+
message,
|
|
92
94
|
e.code(),
|
|
93
95
|
already_started.start_request_id,
|
|
94
96
|
already_started.run_id,
|
|
@@ -97,19 +99,19 @@ def map_error(e: AioRpcError) -> error.CadenceRpcError:
|
|
|
97
99
|
not_exists = error_pb2.EntityNotExistsError()
|
|
98
100
|
details.Unpack(not_exists)
|
|
99
101
|
return error.EntityNotExistsError(
|
|
100
|
-
|
|
102
|
+
message,
|
|
101
103
|
e.code(),
|
|
102
104
|
not_exists.current_cluster,
|
|
103
105
|
not_exists.active_cluster,
|
|
104
106
|
list(not_exists.active_clusters),
|
|
105
107
|
)
|
|
106
108
|
elif details.Is(error_pb2.WorkflowExecutionAlreadyCompletedError.DESCRIPTOR):
|
|
107
|
-
return error.WorkflowExecutionAlreadyCompletedError(
|
|
109
|
+
return error.WorkflowExecutionAlreadyCompletedError(message, e.code())
|
|
108
110
|
elif details.Is(error_pb2.DomainNotActiveError.DESCRIPTOR):
|
|
109
111
|
not_active = error_pb2.DomainNotActiveError()
|
|
110
112
|
details.Unpack(not_active)
|
|
111
113
|
return error.DomainNotActiveError(
|
|
112
|
-
|
|
114
|
+
message,
|
|
113
115
|
e.code(),
|
|
114
116
|
not_active.domain,
|
|
115
117
|
not_active.current_cluster,
|
|
@@ -120,7 +122,7 @@ def map_error(e: AioRpcError) -> error.CadenceRpcError:
|
|
|
120
122
|
not_supported = error_pb2.ClientVersionNotSupportedError()
|
|
121
123
|
details.Unpack(not_supported)
|
|
122
124
|
return error.ClientVersionNotSupportedError(
|
|
123
|
-
|
|
125
|
+
message,
|
|
124
126
|
e.code(),
|
|
125
127
|
not_supported.feature_version,
|
|
126
128
|
not_supported.client_impl,
|
|
@@ -129,20 +131,18 @@ def map_error(e: AioRpcError) -> error.CadenceRpcError:
|
|
|
129
131
|
elif details.Is(error_pb2.FeatureNotEnabledError.DESCRIPTOR):
|
|
130
132
|
not_enabled = error_pb2.FeatureNotEnabledError()
|
|
131
133
|
details.Unpack(not_enabled)
|
|
132
|
-
return error.FeatureNotEnabledError(
|
|
133
|
-
e.details(), e.code(), not_enabled.feature_flag
|
|
134
|
-
)
|
|
134
|
+
return error.FeatureNotEnabledError(message, e.code(), not_enabled.feature_flag)
|
|
135
135
|
elif details.Is(error_pb2.CancellationAlreadyRequestedError.DESCRIPTOR):
|
|
136
|
-
return error.CancellationAlreadyRequestedError(
|
|
136
|
+
return error.CancellationAlreadyRequestedError(message, e.code())
|
|
137
137
|
elif details.Is(error_pb2.DomainAlreadyExistsError.DESCRIPTOR):
|
|
138
|
-
return error.DomainAlreadyExistsError(
|
|
138
|
+
return error.DomainAlreadyExistsError(message, e.code())
|
|
139
139
|
elif details.Is(error_pb2.LimitExceededError.DESCRIPTOR):
|
|
140
|
-
return error.LimitExceededError(
|
|
140
|
+
return error.LimitExceededError(message, e.code())
|
|
141
141
|
elif details.Is(error_pb2.QueryFailedError.DESCRIPTOR):
|
|
142
|
-
return error.QueryFailedError(
|
|
142
|
+
return error.QueryFailedError(message, e.code())
|
|
143
143
|
elif details.Is(error_pb2.ServiceBusyError.DESCRIPTOR):
|
|
144
144
|
service_busy = error_pb2.ServiceBusyError()
|
|
145
145
|
details.Unpack(service_busy)
|
|
146
|
-
return error.ServiceBusyError(
|
|
146
|
+
return error.ServiceBusyError(message, e.code(), service_busy.reason)
|
|
147
147
|
else:
|
|
148
|
-
return error.CadenceRpcError(
|
|
148
|
+
return error.CadenceRpcError(message, e.code())
|
cadence/_internal/rpc/retry.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
-
from typing import Callable, Any
|
|
3
|
+
from typing import Callable, Any, cast
|
|
4
4
|
|
|
5
5
|
from grpc import StatusCode
|
|
6
6
|
from grpc.aio import UnaryUnaryClientInterceptor, ClientCallDetails
|
|
@@ -59,19 +59,23 @@ class RetryInterceptor(UnaryUnaryClientInterceptor):
|
|
|
59
59
|
request: Any,
|
|
60
60
|
) -> Any:
|
|
61
61
|
loop = asyncio.get_running_loop()
|
|
62
|
-
expiration_interval =
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
expiration_interval = (
|
|
63
|
+
client_call_details.timeout
|
|
64
|
+
if client_call_details.timeout is not None
|
|
65
|
+
else float("inf")
|
|
66
|
+
)
|
|
65
67
|
start_time = loop.time()
|
|
66
68
|
deadline = start_time + expiration_interval
|
|
67
69
|
|
|
68
70
|
attempts = 0
|
|
69
71
|
while True:
|
|
70
72
|
remaining = deadline - loop.time()
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
call_details = ClientCallDetails(
|
|
74
|
+
method=client_call_details.method,
|
|
75
|
+
timeout=remaining,
|
|
76
|
+
metadata=client_call_details.metadata,
|
|
77
|
+
credentials=client_call_details.credentials,
|
|
78
|
+
wait_for_ready=client_call_details.wait_for_ready,
|
|
75
79
|
)
|
|
76
80
|
rpc_call = await continuation(call_details, request)
|
|
77
81
|
try:
|
|
@@ -98,11 +102,11 @@ class RetryInterceptor(UnaryUnaryClientInterceptor):
|
|
|
98
102
|
|
|
99
103
|
|
|
100
104
|
def is_retryable(err: CadenceRpcError, call_details: ClientCallDetails) -> bool:
|
|
101
|
-
# Handle requests to the passive side, matching the Go and Java Clients
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
):
|
|
105
|
+
# Handle requests to the passive side, matching the Go and Java Clients.
|
|
106
|
+
# grpc-stubs types method as str, but grpcio corrected it to bytes in v1.75.0
|
|
107
|
+
# (grpc/grpc#39405). Cast to bytes to match the actual runtime type.
|
|
108
|
+
method = cast(bytes, call_details.method)
|
|
109
|
+
if method == GET_WORKFLOW_HISTORY and isinstance(err, EntityNotExistsError):
|
|
106
110
|
return (
|
|
107
111
|
err.active_cluster is not None
|
|
108
112
|
and err.current_cluster is not None
|
cadence/_internal/rpc/yarpc.py
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any, Callable
|
|
1
|
+
from typing import Any, Callable
|
|
2
2
|
|
|
3
3
|
from grpc.aio import Metadata
|
|
4
4
|
from grpc.aio import UnaryUnaryClientInterceptor, ClientCallDetails
|
|
@@ -38,11 +38,10 @@ class YarpcMetadataInterceptor(UnaryUnaryClientInterceptor):
|
|
|
38
38
|
else:
|
|
39
39
|
metadata += self._metadata
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
client_call_details.
|
|
46
|
-
|
|
47
|
-
),
|
|
41
|
+
return ClientCallDetails(
|
|
42
|
+
method=client_call_details.method,
|
|
43
|
+
timeout=client_call_details.timeout or 60.0,
|
|
44
|
+
metadata=metadata,
|
|
45
|
+
credentials=client_call_details.credentials,
|
|
46
|
+
wait_for_ready=client_call_details.wait_for_ready,
|
|
48
47
|
)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from contextlib import contextmanager
|
|
2
4
|
from asyncio import get_running_loop
|
|
3
5
|
from datetime import timedelta
|
|
@@ -5,17 +7,23 @@ from math import ceil
|
|
|
5
7
|
from typing import Iterator, Optional, Any, Unpack, Type, cast, Callable
|
|
6
8
|
|
|
7
9
|
from cadence._internal.workflow.deterministic_event_loop import DeterministicEventLoop
|
|
10
|
+
from cadence._internal.workflow.memo import memo_to_proto
|
|
8
11
|
from cadence._internal.workflow.retry_policy import retry_policy_to_proto
|
|
9
12
|
from cadence._internal.workflow.statemachine.decision_manager import DecisionManager
|
|
10
|
-
from cadence.api.v1
|
|
13
|
+
from cadence.api.v1 import workflow_pb2
|
|
14
|
+
from cadence.api.v1.common_pb2 import ActivityType, WorkflowType, WorkflowExecution
|
|
11
15
|
from cadence.api.v1.decision_pb2 import (
|
|
12
16
|
ScheduleActivityTaskDecisionAttributes,
|
|
17
|
+
SignalExternalWorkflowExecutionDecisionAttributes,
|
|
18
|
+
StartChildWorkflowExecutionDecisionAttributes,
|
|
13
19
|
StartTimerDecisionAttributes,
|
|
14
20
|
)
|
|
15
21
|
from cadence.api.v1.tasklist_pb2 import TaskList, TaskListKind
|
|
16
22
|
from cadence.data_converter import DataConverter
|
|
17
23
|
from cadence.workflow import (
|
|
18
24
|
ActivityOptions,
|
|
25
|
+
ChildWorkflowFuture,
|
|
26
|
+
ChildWorkflowOptions,
|
|
19
27
|
ResultType,
|
|
20
28
|
WorkflowContext,
|
|
21
29
|
WorkflowInfo,
|
|
@@ -105,6 +113,131 @@ class Context(WorkflowContext):
|
|
|
105
113
|
|
|
106
114
|
return cast(ResultType, result)
|
|
107
115
|
|
|
116
|
+
async def execute_child_workflow(
|
|
117
|
+
self,
|
|
118
|
+
workflow_type: str,
|
|
119
|
+
result_type: Type[ResultType],
|
|
120
|
+
*args: Any,
|
|
121
|
+
**kwargs: Unpack[ChildWorkflowOptions],
|
|
122
|
+
) -> ResultType:
|
|
123
|
+
future = await self.start_child_workflow(
|
|
124
|
+
workflow_type, result_type, *args, **kwargs
|
|
125
|
+
)
|
|
126
|
+
return await future
|
|
127
|
+
|
|
128
|
+
async def start_child_workflow(
|
|
129
|
+
self,
|
|
130
|
+
workflow_type: str,
|
|
131
|
+
result_type: Type[ResultType],
|
|
132
|
+
*args: Any,
|
|
133
|
+
**kwargs: Unpack[ChildWorkflowOptions],
|
|
134
|
+
) -> ChildWorkflowFuture[ResultType]:
|
|
135
|
+
schedule_attributes = self._build_child_workflow_attrs(
|
|
136
|
+
workflow_type, *args, **kwargs
|
|
137
|
+
)
|
|
138
|
+
execution_future, result_future = (
|
|
139
|
+
self._decision_manager.schedule_child_workflow(
|
|
140
|
+
schedule_attributes,
|
|
141
|
+
parent_workflow_run_id=self._info.workflow_run_id,
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
workflow_execution = await execution_future
|
|
145
|
+
return ChildWorkflowFuture(
|
|
146
|
+
workflow_id=workflow_execution.workflow_id,
|
|
147
|
+
run_id=workflow_execution.run_id,
|
|
148
|
+
result_future=result_future,
|
|
149
|
+
result_type=result_type,
|
|
150
|
+
data_converter=self.data_converter(),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
def _build_child_workflow_attrs(
|
|
154
|
+
self,
|
|
155
|
+
workflow_type: str,
|
|
156
|
+
*args: Any,
|
|
157
|
+
**kwargs: Unpack[ChildWorkflowOptions],
|
|
158
|
+
) -> StartChildWorkflowExecutionDecisionAttributes:
|
|
159
|
+
execution_timeout = kwargs.get("execution_start_to_close_timeout")
|
|
160
|
+
if execution_timeout is None:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"execution_start_to_close_timeout is required for child workflow execution"
|
|
163
|
+
)
|
|
164
|
+
if execution_timeout <= timedelta(0):
|
|
165
|
+
raise ValueError("execution_start_to_close_timeout must be greater than 0")
|
|
166
|
+
|
|
167
|
+
task_timeout = kwargs.get("task_start_to_close_timeout", timedelta(seconds=10))
|
|
168
|
+
if task_timeout <= timedelta(0):
|
|
169
|
+
raise ValueError("task_start_to_close_timeout must be greater than 0")
|
|
170
|
+
|
|
171
|
+
domain = kwargs.get("domain") or self._info.workflow_domain
|
|
172
|
+
task_list = kwargs.get("task_list") or self._info.workflow_task_list
|
|
173
|
+
|
|
174
|
+
workflow_id = kwargs.get("workflow_id") or ""
|
|
175
|
+
|
|
176
|
+
parent_close_policy = kwargs.get(
|
|
177
|
+
"parent_close_policy",
|
|
178
|
+
workflow_pb2.PARENT_CLOSE_POLICY_TERMINATE,
|
|
179
|
+
)
|
|
180
|
+
workflow_id_reuse_policy = kwargs.get(
|
|
181
|
+
"workflow_id_reuse_policy",
|
|
182
|
+
workflow_pb2.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY,
|
|
183
|
+
)
|
|
184
|
+
if workflow_id_reuse_policy == workflow_pb2.WORKFLOW_ID_REUSE_POLICY_INVALID:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
"workflow_id_reuse_policy cannot be WORKFLOW_ID_REUSE_POLICY_INVALID"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
child_input = self.data_converter().to_data(list(args))
|
|
190
|
+
schedule_attributes = StartChildWorkflowExecutionDecisionAttributes(
|
|
191
|
+
domain=domain,
|
|
192
|
+
workflow_id=workflow_id,
|
|
193
|
+
workflow_type=WorkflowType(name=workflow_type),
|
|
194
|
+
task_list=TaskList(kind=TaskListKind.TASK_LIST_KIND_NORMAL, name=task_list),
|
|
195
|
+
input=child_input,
|
|
196
|
+
execution_start_to_close_timeout=_round_to_nearest_second(
|
|
197
|
+
execution_timeout
|
|
198
|
+
),
|
|
199
|
+
task_start_to_close_timeout=_round_to_nearest_second(task_timeout),
|
|
200
|
+
parent_close_policy=parent_close_policy,
|
|
201
|
+
workflow_id_reuse_policy=workflow_id_reuse_policy,
|
|
202
|
+
retry_policy=retry_policy_to_proto(kwargs.get("retry_policy")),
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
cron_schedule = kwargs.get("cron_schedule")
|
|
206
|
+
if cron_schedule:
|
|
207
|
+
schedule_attributes.cron_schedule = cron_schedule
|
|
208
|
+
|
|
209
|
+
memo_proto = memo_to_proto(self.data_converter(), kwargs.get("memo"))
|
|
210
|
+
if memo_proto is not None:
|
|
211
|
+
schedule_attributes.memo.CopyFrom(memo_proto)
|
|
212
|
+
|
|
213
|
+
return schedule_attributes
|
|
214
|
+
|
|
215
|
+
async def signal_child_workflow(
|
|
216
|
+
self,
|
|
217
|
+
child_workflow_id: str,
|
|
218
|
+
signal_name: str,
|
|
219
|
+
*args: Any,
|
|
220
|
+
) -> None:
|
|
221
|
+
if not child_workflow_id:
|
|
222
|
+
raise ValueError("child_workflow_id must not be empty")
|
|
223
|
+
await self._signal_workflow(
|
|
224
|
+
child_workflow_id, signal_name, args, child_workflow_only=True
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
async def signal_external_workflow(
|
|
228
|
+
self,
|
|
229
|
+
workflow_id: str,
|
|
230
|
+
signal_name: str,
|
|
231
|
+
*args: Any,
|
|
232
|
+
run_id: str = "",
|
|
233
|
+
domain: str = "",
|
|
234
|
+
) -> None:
|
|
235
|
+
if not workflow_id:
|
|
236
|
+
raise ValueError("workflow_id must not be empty")
|
|
237
|
+
await self._signal_workflow(
|
|
238
|
+
workflow_id, signal_name, args, run_id=run_id, domain=domain
|
|
239
|
+
)
|
|
240
|
+
|
|
108
241
|
async def start_timer(self, duration: timedelta):
|
|
109
242
|
if duration.total_seconds() <= 0: # shortcut
|
|
110
243
|
return
|
|
@@ -143,6 +276,30 @@ class Context(WorkflowContext):
|
|
|
143
276
|
finally:
|
|
144
277
|
WorkflowContext._var.reset(token)
|
|
145
278
|
|
|
279
|
+
async def _signal_workflow(
|
|
280
|
+
self,
|
|
281
|
+
workflow_id: str,
|
|
282
|
+
signal_name: str,
|
|
283
|
+
args: tuple,
|
|
284
|
+
*,
|
|
285
|
+
run_id: str = "",
|
|
286
|
+
domain: str = "",
|
|
287
|
+
child_workflow_only: bool = False,
|
|
288
|
+
) -> None:
|
|
289
|
+
if not signal_name:
|
|
290
|
+
raise ValueError("signal_name must not be empty")
|
|
291
|
+
attrs = SignalExternalWorkflowExecutionDecisionAttributes(
|
|
292
|
+
domain=domain or self._info.workflow_domain,
|
|
293
|
+
workflow_execution=WorkflowExecution(
|
|
294
|
+
workflow_id=workflow_id,
|
|
295
|
+
run_id=run_id,
|
|
296
|
+
),
|
|
297
|
+
signal_name=signal_name,
|
|
298
|
+
input=self.data_converter().to_data(list(args)),
|
|
299
|
+
child_workflow_only=child_workflow_only,
|
|
300
|
+
)
|
|
301
|
+
await self._decision_manager.signal_external_workflow(attrs)
|
|
302
|
+
|
|
146
303
|
|
|
147
304
|
def _round_to_nearest_second(delta: timedelta) -> timedelta:
|
|
148
305
|
return timedelta(seconds=ceil(delta.total_seconds()))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Convert user memo maps to/from protobuf :class:`cadence.api.v1.common_pb2.Memo`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Mapping
|
|
6
|
+
|
|
7
|
+
from cadence.api.v1 import common_pb2
|
|
8
|
+
from cadence.data_converter import DataConverter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def memo_to_proto(
|
|
12
|
+
data_converter: DataConverter,
|
|
13
|
+
memo: Mapping[str, Any] | None,
|
|
14
|
+
) -> common_pb2.Memo | None:
|
|
15
|
+
"""Serialize ``memo`` to protobuf, or ``None`` if no memo was provided.
|
|
16
|
+
|
|
17
|
+
Each value is encoded as a single-element list via the data converter,
|
|
18
|
+
matching Go/Java SDK behavior (one :class:`Payload` per key).
|
|
19
|
+
"""
|
|
20
|
+
if not memo:
|
|
21
|
+
return None
|
|
22
|
+
out = common_pb2.Memo()
|
|
23
|
+
for key, value in memo.items():
|
|
24
|
+
out.fields[key].CopyFrom(data_converter.to_data([value]))
|
|
25
|
+
return out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def memo_from_proto(
|
|
29
|
+
data_converter: DataConverter,
|
|
30
|
+
memo: common_pb2.Memo,
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
"""Deserialize a protobuf ``Memo`` back to a plain dict.
|
|
33
|
+
|
|
34
|
+
Each field payload was encoded as a single-element list; we unwrap that
|
|
35
|
+
first element to recover the original value.
|
|
36
|
+
"""
|
|
37
|
+
return {
|
|
38
|
+
key: data_converter.from_data(payload, [None])[0]
|
|
39
|
+
for key, payload in memo.fields.items()
|
|
40
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from cadence._internal.workflow.statemachine.decision_state_machine import (
|
|
4
|
+
BaseDecisionStateMachine,
|
|
5
|
+
DecisionFuture,
|
|
6
|
+
DecisionId,
|
|
7
|
+
DecisionState,
|
|
8
|
+
DecisionType,
|
|
9
|
+
)
|
|
10
|
+
from cadence._internal.workflow.statemachine.event_dispatcher import EventDispatcher
|
|
11
|
+
from cadence._internal.workflow.statemachine.nondeterminism import (
|
|
12
|
+
record_immediate_cancel,
|
|
13
|
+
)
|
|
14
|
+
from cadence.api.v1 import decision, history
|
|
15
|
+
from cadence.api.v1.common_pb2 import Payload, WorkflowExecution
|
|
16
|
+
from cadence.error import (
|
|
17
|
+
ChildWorkflowExecutionCanceled,
|
|
18
|
+
ChildWorkflowExecutionFailed,
|
|
19
|
+
ChildWorkflowExecutionTerminated,
|
|
20
|
+
ChildWorkflowExecutionTimedOut,
|
|
21
|
+
StartChildWorkflowExecutionFailed,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
child_workflow_events = EventDispatcher("initiated_event_id")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ChildWorkflowExecutionStateMachine(BaseDecisionStateMachine):
|
|
28
|
+
"""State machine for StartChildWorkflowExecution and child close events."""
|
|
29
|
+
|
|
30
|
+
request: decision.StartChildWorkflowExecutionDecisionAttributes
|
|
31
|
+
execution: DecisionFuture[WorkflowExecution]
|
|
32
|
+
result: DecisionFuture[Payload]
|
|
33
|
+
_run_id: str | None
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
request: decision.StartChildWorkflowExecutionDecisionAttributes,
|
|
38
|
+
execution: DecisionFuture[WorkflowExecution],
|
|
39
|
+
result: DecisionFuture[Payload],
|
|
40
|
+
) -> None:
|
|
41
|
+
super().__init__()
|
|
42
|
+
self.request = request
|
|
43
|
+
self.execution = execution
|
|
44
|
+
self.result = result
|
|
45
|
+
self._run_id = None
|
|
46
|
+
|
|
47
|
+
def get_id(self) -> DecisionId:
|
|
48
|
+
return DecisionId(DecisionType.CHILD_WORKFLOW, self.request.workflow_id)
|
|
49
|
+
|
|
50
|
+
def get_decision(self) -> decision.Decision | None:
|
|
51
|
+
match self.state:
|
|
52
|
+
case DecisionState.REQUESTED:
|
|
53
|
+
return decision.Decision(
|
|
54
|
+
start_child_workflow_execution_decision_attributes=self.request
|
|
55
|
+
)
|
|
56
|
+
case DecisionState.CANCELED_AFTER_REQUESTED:
|
|
57
|
+
return record_immediate_cancel(self.request)
|
|
58
|
+
case (
|
|
59
|
+
DecisionState.CANCELED_AFTER_RECORDED
|
|
60
|
+
| DecisionState.CANCELED_AFTER_STARTED
|
|
61
|
+
):
|
|
62
|
+
return decision.Decision(
|
|
63
|
+
request_cancel_external_workflow_execution_decision_attributes=decision.RequestCancelExternalWorkflowExecutionDecisionAttributes(
|
|
64
|
+
domain=self.request.domain,
|
|
65
|
+
workflow_execution=WorkflowExecution(
|
|
66
|
+
workflow_id=self.request.workflow_id,
|
|
67
|
+
run_id=self._run_id or "",
|
|
68
|
+
),
|
|
69
|
+
child_workflow_only=True,
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
case _:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def request_cancel(self) -> bool:
|
|
76
|
+
match self.state:
|
|
77
|
+
case DecisionState.REQUESTED:
|
|
78
|
+
self._transition(DecisionState.CANCELED_AFTER_REQUESTED)
|
|
79
|
+
self.execution.force_cancel()
|
|
80
|
+
self.result.force_cancel()
|
|
81
|
+
return True
|
|
82
|
+
case DecisionState.RECORDED:
|
|
83
|
+
self._transition(DecisionState.CANCELED_AFTER_RECORDED)
|
|
84
|
+
self.execution.force_cancel()
|
|
85
|
+
return True
|
|
86
|
+
case DecisionState.STARTED:
|
|
87
|
+
self._transition(DecisionState.CANCELED_AFTER_STARTED)
|
|
88
|
+
return True
|
|
89
|
+
case _:
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
@child_workflow_events.event("workflow_id", event_id_is_alias=True)
|
|
93
|
+
def handle_initiated(
|
|
94
|
+
self, _: history.StartChildWorkflowExecutionInitiatedEventAttributes
|
|
95
|
+
) -> None:
|
|
96
|
+
self._transition(DecisionState.RECORDED)
|
|
97
|
+
|
|
98
|
+
@child_workflow_events.event()
|
|
99
|
+
def handle_initiation_failed(
|
|
100
|
+
self, event: history.StartChildWorkflowExecutionFailedEventAttributes
|
|
101
|
+
) -> None:
|
|
102
|
+
self._transition(DecisionState.COMPLETED)
|
|
103
|
+
exc = StartChildWorkflowExecutionFailed(
|
|
104
|
+
f"start child failed: {event.cause}",
|
|
105
|
+
cause=event.cause,
|
|
106
|
+
workflow_id=event.workflow_id,
|
|
107
|
+
)
|
|
108
|
+
self.execution.set_exception(exc)
|
|
109
|
+
self.result.set_exception(exc)
|
|
110
|
+
|
|
111
|
+
@child_workflow_events.event()
|
|
112
|
+
def handle_started(
|
|
113
|
+
self, event: history.ChildWorkflowExecutionStartedEventAttributes
|
|
114
|
+
) -> None:
|
|
115
|
+
self._run_id = event.workflow_execution.run_id
|
|
116
|
+
if self.state is DecisionState.CANCELED_AFTER_RECORDED:
|
|
117
|
+
self._transition(DecisionState.CANCELED_AFTER_STARTED)
|
|
118
|
+
else:
|
|
119
|
+
self._transition(DecisionState.STARTED)
|
|
120
|
+
if not self.execution.done():
|
|
121
|
+
self.execution.set_result(event.workflow_execution)
|
|
122
|
+
|
|
123
|
+
@child_workflow_events.event()
|
|
124
|
+
def handle_completed(
|
|
125
|
+
self, event: history.ChildWorkflowExecutionCompletedEventAttributes
|
|
126
|
+
) -> None:
|
|
127
|
+
self._transition(DecisionState.COMPLETED)
|
|
128
|
+
self.result.set_result(event.result)
|
|
129
|
+
|
|
130
|
+
@child_workflow_events.event()
|
|
131
|
+
def handle_failed(
|
|
132
|
+
self, event: history.ChildWorkflowExecutionFailedEventAttributes
|
|
133
|
+
) -> None:
|
|
134
|
+
self._transition(DecisionState.COMPLETED)
|
|
135
|
+
self.result.set_exception(
|
|
136
|
+
ChildWorkflowExecutionFailed(
|
|
137
|
+
event.failure.reason,
|
|
138
|
+
failure=event.failure,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
@child_workflow_events.event()
|
|
143
|
+
def handle_canceled(
|
|
144
|
+
self, event: history.ChildWorkflowExecutionCanceledEventAttributes
|
|
145
|
+
) -> None:
|
|
146
|
+
self._transition(DecisionState.COMPLETED)
|
|
147
|
+
self.result.set_exception(
|
|
148
|
+
ChildWorkflowExecutionCanceled(
|
|
149
|
+
"child workflow canceled", details=event.details
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
@child_workflow_events.event()
|
|
154
|
+
def handle_timed_out(
|
|
155
|
+
self, event: history.ChildWorkflowExecutionTimedOutEventAttributes
|
|
156
|
+
) -> None:
|
|
157
|
+
self._transition(DecisionState.COMPLETED)
|
|
158
|
+
self.result.set_exception(
|
|
159
|
+
ChildWorkflowExecutionTimedOut(
|
|
160
|
+
f"child workflow timed out: {event.timeout_type}",
|
|
161
|
+
timeout_type=int(event.timeout_type),
|
|
162
|
+
)
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
@child_workflow_events.event()
|
|
166
|
+
def handle_terminated(
|
|
167
|
+
self, event: history.ChildWorkflowExecutionTerminatedEventAttributes
|
|
168
|
+
) -> None:
|
|
169
|
+
self._transition(DecisionState.COMPLETED)
|
|
170
|
+
self.result.set_exception(ChildWorkflowExecutionTerminated())
|
|
171
|
+
|
|
172
|
+
@child_workflow_events.event(
|
|
173
|
+
"workflow_execution.workflow_id", event_id_is_alias=True
|
|
174
|
+
)
|
|
175
|
+
def handle_cancel_initiated(
|
|
176
|
+
self, _: history.RequestCancelExternalWorkflowExecutionInitiatedEventAttributes
|
|
177
|
+
) -> None:
|
|
178
|
+
self._transition(DecisionState.CANCELLATION_RECORDED)
|
|
179
|
+
|
|
180
|
+
@child_workflow_events.event()
|
|
181
|
+
def handle_cancel_failed(
|
|
182
|
+
self, _: history.RequestCancelExternalWorkflowExecutionFailedEventAttributes
|
|
183
|
+
) -> None:
|
|
184
|
+
self._transition(DecisionState.STARTED)
|