dex-python-sdk 0.2.11__tar.gz → 0.3.1__tar.gz
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.
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/PKG-INFO +1 -1
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/__init__.py +2 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_async_value_hydrator.py +28 -2
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_async_worker_dispatcher.py +61 -8
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_invocation_context.py +25 -24
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_value_hydrator.py +28 -2
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_worker_dispatcher.py +205 -6
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/async_client.py +26 -1
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/client.py +26 -1
- dex_python_sdk-0.3.1/dex/dexpb/dex_pb2.py +495 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/dexpb/dex_pb2.pyi +124 -22
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow.py +35 -4
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow_options.py +69 -3
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/step.py +26 -2
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/pyproject.toml +1 -1
- dex_python_sdk-0.2.11/dex/dexpb/dex_pb2.py +0 -477
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/LEGACY_NOTICES.md +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/LICENSE +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/README.md +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_async_worker_service.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_grpc_errors.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_native.pyi +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_utils.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_value_mapper.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/_worker_service.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/async_worker.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/attribute.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/blob_cache.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/channel.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/client_options.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/codec.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/condition.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/context.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/dexpb/__init__.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/dexpb/dex_pb2_grpc.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow_config.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow_info.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/flow_result.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/py.typed +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/runtime_errors.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/step_execution.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/stream.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/subflow.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/timer.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/wait.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/worker.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/dex/worker_options.py +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/Cargo.lock +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/Cargo.toml +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/Cargo.toml +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/LICENSE +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/config.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/entry.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/error.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/format.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/lib.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/policy.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/src/store.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache/tests/blob_cache_integration.rs +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache-python/Cargo.toml +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache-python/LICENSE +0 -0
- {dex_python_sdk-0.2.11 → dex_python_sdk-0.3.1}/sdk-rust/crates/dex-blob-cache-python/src/lib.rs +0 -0
|
@@ -47,6 +47,7 @@ from dex.flow_info import (
|
|
|
47
47
|
SearchFlowsPage,
|
|
48
48
|
)
|
|
49
49
|
from dex.flow_options import (
|
|
50
|
+
FlowTimeoutHandlerOptions,
|
|
50
51
|
FlowTimeoutPolicy,
|
|
51
52
|
IdReusePolicy,
|
|
52
53
|
StartFlowOptions,
|
|
@@ -152,6 +153,7 @@ __all__ = [
|
|
|
152
153
|
"FlowNotActiveError",
|
|
153
154
|
"FlowNotFoundError",
|
|
154
155
|
"FlowStatus",
|
|
156
|
+
"FlowTimeoutHandlerOptions",
|
|
155
157
|
"FlowTimeoutPolicy",
|
|
156
158
|
"HealthInfo",
|
|
157
159
|
"IdReusePolicy",
|
|
@@ -80,12 +80,25 @@ class AsyncValueHydrator:
|
|
|
80
80
|
if has_heartbeat:
|
|
81
81
|
values.append(request.context.last_heartbeat_value)
|
|
82
82
|
values.extend(entry.value for entry in request.attributes)
|
|
83
|
+
channel_messages = [
|
|
84
|
+
message
|
|
85
|
+
for channel_values in request.loaded_channel_messages.values()
|
|
86
|
+
for message in channel_values.messages
|
|
87
|
+
]
|
|
88
|
+
values.extend(message.value for message in channel_messages)
|
|
83
89
|
hydrated = iter(await self.hydrate_all(values))
|
|
84
90
|
result.step_input.CopyFrom(next(hydrated))
|
|
85
91
|
if has_heartbeat:
|
|
86
92
|
result.context.last_heartbeat_value.CopyFrom(next(hydrated))
|
|
87
|
-
for entry
|
|
88
|
-
entry.value.CopyFrom(
|
|
93
|
+
for entry in result.attributes:
|
|
94
|
+
entry.value.CopyFrom(next(hydrated))
|
|
95
|
+
result_messages = [
|
|
96
|
+
message
|
|
97
|
+
for channel_values in result.loaded_channel_messages.values()
|
|
98
|
+
for message in channel_values.messages
|
|
99
|
+
]
|
|
100
|
+
for message in result_messages:
|
|
101
|
+
message.value.CopyFrom(next(hydrated))
|
|
89
102
|
return result
|
|
90
103
|
|
|
91
104
|
async def execute_request(
|
|
@@ -101,6 +114,12 @@ class AsyncValueHydrator:
|
|
|
101
114
|
values.append(request.context.last_heartbeat_value)
|
|
102
115
|
values.extend(entry.value for entry in request.attributes)
|
|
103
116
|
values.extend(entry.value for entry in request.step_exe_locals)
|
|
117
|
+
channel_messages = [
|
|
118
|
+
message
|
|
119
|
+
for channel_values in request.loaded_channel_messages.values()
|
|
120
|
+
for message in channel_values.messages
|
|
121
|
+
]
|
|
122
|
+
values.extend(message.value for message in channel_messages)
|
|
104
123
|
for channel_result in request.condition_results.channel_results:
|
|
105
124
|
values.extend(channel_result.values)
|
|
106
125
|
hydrated = iter(await self.hydrate_all(values))
|
|
@@ -112,6 +131,13 @@ class AsyncValueHydrator:
|
|
|
112
131
|
entry.value.CopyFrom(next(hydrated))
|
|
113
132
|
for entry in result.step_exe_locals:
|
|
114
133
|
entry.value.CopyFrom(next(hydrated))
|
|
134
|
+
result_messages = [
|
|
135
|
+
message
|
|
136
|
+
for channel_values in result.loaded_channel_messages.values()
|
|
137
|
+
for message in channel_values.messages
|
|
138
|
+
]
|
|
139
|
+
for message in result_messages:
|
|
140
|
+
message.value.CopyFrom(next(hydrated))
|
|
115
141
|
for channel_result in result.condition_results.channel_results:
|
|
116
142
|
for value in channel_result.values:
|
|
117
143
|
value.CopyFrom(next(hydrated))
|
|
@@ -100,6 +100,11 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
100
100
|
request.context,
|
|
101
101
|
self._values,
|
|
102
102
|
request.attributes,
|
|
103
|
+
channel_infos=dict(request.channel_infos),
|
|
104
|
+
loaded_channel_messages=dict(request.loaded_channel_messages),
|
|
105
|
+
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
106
|
+
loaded_channel_names=request.loaded_channel_names,
|
|
107
|
+
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
103
108
|
is_active=is_active,
|
|
104
109
|
output_emitter=emitter,
|
|
105
110
|
)
|
|
@@ -126,6 +131,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
126
131
|
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
127
132
|
record_events=context.events,
|
|
128
133
|
publish_to_channel=context.publications,
|
|
134
|
+
delete_from_channel=context.channel_deletions,
|
|
129
135
|
)
|
|
130
136
|
waiting = self._map_wait(flow, wait)
|
|
131
137
|
if waiting is not None:
|
|
@@ -153,8 +159,16 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
153
159
|
is_active: Callable[[], bool] | None = None,
|
|
154
160
|
) -> AsyncGenerator[pb.InvokeExecuteMethodOutput, None]:
|
|
155
161
|
if original.step_type == _TIMEOUT_HANDLER_STEP_TYPE:
|
|
156
|
-
|
|
157
|
-
|
|
162
|
+
emitter = _AsyncStepOutputEmitter()
|
|
163
|
+
handler = asyncio.create_task(
|
|
164
|
+
self._invoke_timeout_handler_async(original, emitter, is_active)
|
|
165
|
+
)
|
|
166
|
+
try:
|
|
167
|
+
async for output in self._drain_outputs(emitter, handler):
|
|
168
|
+
yield self._map_execute_output(output)
|
|
169
|
+
yield pb.InvokeExecuteMethodOutput(result=await handler)
|
|
170
|
+
finally:
|
|
171
|
+
await self._close_invocation(emitter, handler)
|
|
158
172
|
return
|
|
159
173
|
emitter = _AsyncStepOutputEmitter()
|
|
160
174
|
handler = asyncio.create_task(
|
|
@@ -187,6 +201,11 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
187
201
|
request.attributes,
|
|
188
202
|
request.step_exe_locals,
|
|
189
203
|
condition_results,
|
|
204
|
+
channel_infos=dict(request.channel_infos),
|
|
205
|
+
loaded_channel_messages=dict(request.loaded_channel_messages),
|
|
206
|
+
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
207
|
+
loaded_channel_names=request.loaded_channel_names,
|
|
208
|
+
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
190
209
|
is_active=is_active,
|
|
191
210
|
output_emitter=emitter,
|
|
192
211
|
)
|
|
@@ -214,6 +233,7 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
214
233
|
record_events=context.events,
|
|
215
234
|
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
216
235
|
publish_to_channel=context.publications,
|
|
236
|
+
delete_from_channel=context.channel_deletions,
|
|
217
237
|
)
|
|
218
238
|
except InvalidStepResultError as error:
|
|
219
239
|
failure = error
|
|
@@ -235,6 +255,8 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
235
255
|
async def _invoke_timeout_handler_async(
|
|
236
256
|
self,
|
|
237
257
|
original: pb.InvokeExecuteMethodRequest,
|
|
258
|
+
emitter: _AsyncStepOutputEmitter,
|
|
259
|
+
is_active: Callable[[], bool] | None,
|
|
238
260
|
) -> pb.InvokeExecuteMethodResponse:
|
|
239
261
|
request = await self._async_hydrator.execute_request(original)
|
|
240
262
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
@@ -260,24 +282,55 @@ class AsyncWorkerDispatcher(WorkerDispatcher):
|
|
|
260
282
|
request.attributes,
|
|
261
283
|
request.step_exe_locals,
|
|
262
284
|
condition_results,
|
|
285
|
+
channel_infos=dict(request.channel_infos),
|
|
286
|
+
loaded_channel_messages=dict(request.loaded_channel_messages),
|
|
287
|
+
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
288
|
+
loaded_channel_names=request.loaded_channel_names,
|
|
289
|
+
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
290
|
+
is_active=is_active,
|
|
291
|
+
output_emitter=emitter,
|
|
263
292
|
)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
293
|
+
response: pb.InvokeExecuteMethodResponse | None = None
|
|
294
|
+
failure: BaseException | None = None
|
|
295
|
+
cause: BaseException | None = None
|
|
267
296
|
try:
|
|
297
|
+
decision: Any = flow.flow.handle_timeout(context)
|
|
298
|
+
if isgenerator(decision):
|
|
299
|
+
decision.close()
|
|
300
|
+
raise InvalidStepResultError(
|
|
301
|
+
flow.name,
|
|
302
|
+
_TIMEOUT_HANDLER_STEP_TYPE,
|
|
303
|
+
"execute",
|
|
304
|
+
"synchronous generators require Worker",
|
|
305
|
+
)
|
|
306
|
+
if isawaitable(decision):
|
|
307
|
+
decision = await decision
|
|
268
308
|
if not isinstance(decision, StepDecision):
|
|
269
309
|
raise TypeError("handle_timeout must return StepDecision")
|
|
270
|
-
|
|
310
|
+
response = pb.InvokeExecuteMethodResponse(
|
|
271
311
|
step_decision=self._map_decision(flow, decision),
|
|
272
312
|
upsert_attributes=list(context.attribute_writes.values()),
|
|
273
313
|
record_events=context.events,
|
|
274
314
|
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
275
315
|
publish_to_channel=context.publications,
|
|
316
|
+
delete_from_channel=context.channel_deletions,
|
|
276
317
|
)
|
|
318
|
+
except InvalidStepResultError as error:
|
|
319
|
+
failure = error
|
|
277
320
|
except (TypeError, ValueError) as error:
|
|
278
|
-
|
|
321
|
+
failure = InvalidStepResultError(
|
|
279
322
|
flow.name, _TIMEOUT_HANDLER_STEP_TYPE, "execute", str(error)
|
|
280
|
-
)
|
|
323
|
+
)
|
|
324
|
+
cause = error
|
|
325
|
+
except BaseException as error:
|
|
326
|
+
failure = error
|
|
327
|
+
combined = context._finalize_step_outputs(failure)
|
|
328
|
+
if combined is not None:
|
|
329
|
+
if cause is not None:
|
|
330
|
+
raise combined from cause
|
|
331
|
+
raise combined
|
|
332
|
+
assert response is not None
|
|
333
|
+
return response
|
|
281
334
|
|
|
282
335
|
async def invoke_rpc( # type: ignore[override]
|
|
283
336
|
self,
|
|
@@ -139,7 +139,12 @@ class InvocationContext:
|
|
|
139
139
|
|
|
140
140
|
async def heartbeat(self, value: object = _NO_HEARTBEAT_VALUE) -> None:
|
|
141
141
|
if (
|
|
142
|
-
self._method
|
|
142
|
+
self._method
|
|
143
|
+
not in (
|
|
144
|
+
InvocationMethod.WAIT_FOR,
|
|
145
|
+
InvocationMethod.EXECUTE,
|
|
146
|
+
InvocationMethod.TIMEOUT,
|
|
147
|
+
)
|
|
143
148
|
or self._output_emitter is None
|
|
144
149
|
):
|
|
145
150
|
raise ValueError("heartbeat requires an asynchronous Step Context")
|
|
@@ -226,7 +231,11 @@ class InvocationContext:
|
|
|
226
231
|
definition: Stream[ValueT],
|
|
227
232
|
value: ValueT,
|
|
228
233
|
) -> StepOutput | None:
|
|
229
|
-
if self._method not in (
|
|
234
|
+
if self._method not in (
|
|
235
|
+
InvocationMethod.WAIT_FOR,
|
|
236
|
+
InvocationMethod.EXECUTE,
|
|
237
|
+
InvocationMethod.TIMEOUT,
|
|
238
|
+
):
|
|
230
239
|
raise ValueError("Stream writes require a Step Context")
|
|
231
240
|
self._require_registered(definition)
|
|
232
241
|
output = _StreamStepOutput(
|
|
@@ -245,7 +254,11 @@ class InvocationContext:
|
|
|
245
254
|
return None
|
|
246
255
|
|
|
247
256
|
def _prepare_buffered_stream(self, definition: Stream[object]) -> bool:
|
|
248
|
-
if self._method not in (
|
|
257
|
+
if self._method not in (
|
|
258
|
+
InvocationMethod.WAIT_FOR,
|
|
259
|
+
InvocationMethod.EXECUTE,
|
|
260
|
+
InvocationMethod.TIMEOUT,
|
|
261
|
+
):
|
|
249
262
|
raise ValueError("Buffered Streams require a Step Context")
|
|
250
263
|
self._require_registered(definition)
|
|
251
264
|
return self._output_emitter is not None
|
|
@@ -357,11 +370,10 @@ class InvocationContext:
|
|
|
357
370
|
),
|
|
358
371
|
)
|
|
359
372
|
)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
)
|
|
373
|
+
current = self._channel_infos.get(name)
|
|
374
|
+
self._channel_infos[name] = pb.ChannelInfo(
|
|
375
|
+
size=(current.size if current is not None else 0) + 1
|
|
376
|
+
)
|
|
365
377
|
|
|
366
378
|
def _delete_channel_message(
|
|
367
379
|
self,
|
|
@@ -369,8 +381,6 @@ class InvocationContext:
|
|
|
369
381
|
instance: str | None,
|
|
370
382
|
message_id: str,
|
|
371
383
|
) -> None:
|
|
372
|
-
if self._method is not InvocationMethod.RPC:
|
|
373
|
-
raise ValueError("Channel message deletion requires an RPC Context")
|
|
374
384
|
self._require_registered(definition)
|
|
375
385
|
name = self._physical_name(definition, instance)
|
|
376
386
|
self.channel_deletions.append(
|
|
@@ -410,8 +420,6 @@ class InvocationContext:
|
|
|
410
420
|
definition: ChannelMap[object],
|
|
411
421
|
) -> tuple[str, ...]:
|
|
412
422
|
self._require_registered(definition)
|
|
413
|
-
if self._method is not InvocationMethod.RPC:
|
|
414
|
-
raise ValueError("ChannelMap introspection requires an RPC invocation")
|
|
415
423
|
prefix = f"{definition.name}/"
|
|
416
424
|
return tuple(
|
|
417
425
|
sorted(
|
|
@@ -435,8 +443,6 @@ class InvocationContext:
|
|
|
435
443
|
definition: Channel[ValueT] | ChannelMap[ValueT],
|
|
436
444
|
instance: str | None,
|
|
437
445
|
) -> tuple[ChannelMessage[ValueT], ...]:
|
|
438
|
-
if self._method is not InvocationMethod.RPC:
|
|
439
|
-
raise ValueError("pending Channel messages require an RPC Context")
|
|
440
446
|
self._require_registered(definition)
|
|
441
447
|
if isinstance(definition, ChannelMap):
|
|
442
448
|
channel_name = self._physical_name(definition, instance)
|
|
@@ -449,7 +455,7 @@ class InvocationContext:
|
|
|
449
455
|
is_loaded = definition.name in self._loaded_channel_names
|
|
450
456
|
if not is_loaded:
|
|
451
457
|
raise ChannelMessagesNotLoadedError(
|
|
452
|
-
f"Channel messages were not loaded for
|
|
458
|
+
f"Channel messages were not loaded for this invocation: {definition.name}"
|
|
453
459
|
)
|
|
454
460
|
values = self._loaded_channel_messages.get(channel_name)
|
|
455
461
|
if values is None:
|
|
@@ -489,9 +495,7 @@ class InvocationContext:
|
|
|
489
495
|
definition: Attribute[Any] | AttributeMap[Any],
|
|
490
496
|
instance: str | None,
|
|
491
497
|
) -> None:
|
|
492
|
-
if
|
|
493
|
-
definition, AttributeMap
|
|
494
|
-
):
|
|
498
|
+
if not isinstance(definition, AttributeMap):
|
|
495
499
|
return
|
|
496
500
|
physical_name = self._physical_name(definition, instance)
|
|
497
501
|
if (
|
|
@@ -499,19 +503,16 @@ class InvocationContext:
|
|
|
499
503
|
and physical_name not in self._loaded_attribute_map_instances
|
|
500
504
|
):
|
|
501
505
|
raise AttributeMapNotLoadedError(
|
|
502
|
-
f"AttributeMap instance was not loaded for
|
|
506
|
+
f"AttributeMap instance was not loaded for this invocation: {physical_name}"
|
|
503
507
|
)
|
|
504
508
|
|
|
505
509
|
def _require_attribute_map_all_loaded(
|
|
506
510
|
self,
|
|
507
511
|
definition: AttributeMap[object],
|
|
508
512
|
) -> None:
|
|
509
|
-
if
|
|
510
|
-
self._method is InvocationMethod.RPC
|
|
511
|
-
and f"{definition.name}/" not in self._loaded_attribute_map_instances
|
|
512
|
-
):
|
|
513
|
+
if f"{definition.name}/" not in self._loaded_attribute_map_instances:
|
|
513
514
|
raise AttributeMapNotLoadedError(
|
|
514
|
-
f"all AttributeMap instances were not loaded for
|
|
515
|
+
f"all AttributeMap instances were not loaded for this invocation: {definition.name}"
|
|
515
516
|
)
|
|
516
517
|
|
|
517
518
|
def _require_registered(self, definition: _Definition) -> None:
|
|
@@ -80,12 +80,25 @@ class ValueHydrator:
|
|
|
80
80
|
if has_heartbeat:
|
|
81
81
|
values.append(request.context.last_heartbeat_value)
|
|
82
82
|
values.extend(entry.value for entry in request.attributes)
|
|
83
|
+
channel_messages = [
|
|
84
|
+
message
|
|
85
|
+
for channel_values in request.loaded_channel_messages.values()
|
|
86
|
+
for message in channel_values.messages
|
|
87
|
+
]
|
|
88
|
+
values.extend(message.value for message in channel_messages)
|
|
83
89
|
hydrated = iter(self.hydrate_all(values))
|
|
84
90
|
result.step_input.CopyFrom(next(hydrated))
|
|
85
91
|
if has_heartbeat:
|
|
86
92
|
result.context.last_heartbeat_value.CopyFrom(next(hydrated))
|
|
87
|
-
for entry
|
|
88
|
-
entry.value.CopyFrom(
|
|
93
|
+
for entry in result.attributes:
|
|
94
|
+
entry.value.CopyFrom(next(hydrated))
|
|
95
|
+
result_messages = [
|
|
96
|
+
message
|
|
97
|
+
for channel_values in result.loaded_channel_messages.values()
|
|
98
|
+
for message in channel_values.messages
|
|
99
|
+
]
|
|
100
|
+
for message in result_messages:
|
|
101
|
+
message.value.CopyFrom(next(hydrated))
|
|
89
102
|
return result
|
|
90
103
|
|
|
91
104
|
def execute_request(
|
|
@@ -101,6 +114,12 @@ class ValueHydrator:
|
|
|
101
114
|
values.append(request.context.last_heartbeat_value)
|
|
102
115
|
values.extend(entry.value for entry in request.attributes)
|
|
103
116
|
values.extend(entry.value for entry in request.step_exe_locals)
|
|
117
|
+
channel_messages = [
|
|
118
|
+
message
|
|
119
|
+
for channel_values in request.loaded_channel_messages.values()
|
|
120
|
+
for message in channel_values.messages
|
|
121
|
+
]
|
|
122
|
+
values.extend(message.value for message in channel_messages)
|
|
104
123
|
for channel_result in request.condition_results.channel_results:
|
|
105
124
|
values.extend(channel_result.values)
|
|
106
125
|
for flow_result in request.condition_results.sub_flow_results:
|
|
@@ -116,6 +135,13 @@ class ValueHydrator:
|
|
|
116
135
|
entry.value.CopyFrom(next(hydrated))
|
|
117
136
|
for entry in result.step_exe_locals:
|
|
118
137
|
entry.value.CopyFrom(next(hydrated))
|
|
138
|
+
result_messages = [
|
|
139
|
+
message
|
|
140
|
+
for channel_values in result.loaded_channel_messages.values()
|
|
141
|
+
for message in channel_values.messages
|
|
142
|
+
]
|
|
143
|
+
for message in result_messages:
|
|
144
|
+
message.value.CopyFrom(next(hydrated))
|
|
119
145
|
for channel_result in result.condition_results.channel_results:
|
|
120
146
|
for value in channel_result.values:
|
|
121
147
|
value.CopyFrom(next(hydrated))
|
|
@@ -15,13 +15,19 @@ from typing import Any, Callable, Generator, cast
|
|
|
15
15
|
from dex._invocation_context import InvocationContext, InvocationMethod
|
|
16
16
|
from dex._value_hydrator import ValueHydrator
|
|
17
17
|
from dex._value_mapper import ValueMapper
|
|
18
|
-
from dex.attribute import
|
|
19
|
-
|
|
18
|
+
from dex.attribute import (
|
|
19
|
+
AttributeLock,
|
|
20
|
+
AttributeMap,
|
|
21
|
+
AttributeMapLoad,
|
|
22
|
+
_apply_attribute_store_sync,
|
|
23
|
+
)
|
|
24
|
+
from dex.channel import Channel, ChannelMap, ChannelMapLoad
|
|
20
25
|
from dex.condition import ChannelCondition, Condition, SubFlowCondition, TimerCondition
|
|
21
26
|
from dex.dexpb import dex_pb2 as pb
|
|
22
27
|
from dex.flow import Registry, RPCResult, _RegisteredFlow, _RegisteredStep
|
|
23
28
|
from dex.flow_config import ActiveStepSearchMode, FlowConfig
|
|
24
29
|
from dex.flow_options import (
|
|
30
|
+
FlowTimeoutHandlerOptions,
|
|
25
31
|
FlowTimeoutPolicy,
|
|
26
32
|
SubFlowOptions,
|
|
27
33
|
SubFlowReusePolicy,
|
|
@@ -71,6 +77,11 @@ class WorkerDispatcher:
|
|
|
71
77
|
request.context,
|
|
72
78
|
self._values,
|
|
73
79
|
request.attributes,
|
|
80
|
+
channel_infos=dict(request.channel_infos),
|
|
81
|
+
loaded_channel_messages=dict(request.loaded_channel_messages),
|
|
82
|
+
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
83
|
+
loaded_channel_names=request.loaded_channel_names,
|
|
84
|
+
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
74
85
|
is_active=is_active,
|
|
75
86
|
)
|
|
76
87
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
@@ -92,6 +103,7 @@ class WorkerDispatcher:
|
|
|
92
103
|
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
93
104
|
record_events=context.events,
|
|
94
105
|
publish_to_channel=context.publications,
|
|
106
|
+
delete_from_channel=context.channel_deletions,
|
|
95
107
|
)
|
|
96
108
|
waiting = self._map_wait(flow, wait)
|
|
97
109
|
if waiting is not None:
|
|
@@ -110,9 +122,8 @@ class WorkerDispatcher:
|
|
|
110
122
|
request = self._hydrator.execute_request(original)
|
|
111
123
|
flow = self._registry._flow_by_type(request.flow_type)
|
|
112
124
|
if request.step_type == _TIMEOUT_HANDLER_STEP_TYPE:
|
|
113
|
-
yield
|
|
114
|
-
|
|
115
|
-
)
|
|
125
|
+
response = yield from self._invoke_timeout_handler(request, flow)
|
|
126
|
+
yield pb.InvokeExecuteMethodOutput(result=response)
|
|
116
127
|
return
|
|
117
128
|
step = flow.step(request.step_type)
|
|
118
129
|
condition_results = (
|
|
@@ -126,6 +137,11 @@ class WorkerDispatcher:
|
|
|
126
137
|
request.attributes,
|
|
127
138
|
request.step_exe_locals,
|
|
128
139
|
condition_results,
|
|
140
|
+
channel_infos=dict(request.channel_infos),
|
|
141
|
+
loaded_channel_messages=dict(request.loaded_channel_messages),
|
|
142
|
+
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
143
|
+
loaded_channel_names=request.loaded_channel_names,
|
|
144
|
+
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
129
145
|
is_active=is_active,
|
|
130
146
|
)
|
|
131
147
|
input = self._values.decode(request.step_input, step.input_codec)
|
|
@@ -149,6 +165,7 @@ class WorkerDispatcher:
|
|
|
149
165
|
record_events=context.events,
|
|
150
166
|
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
151
167
|
publish_to_channel=context.publications,
|
|
168
|
+
delete_from_channel=context.channel_deletions,
|
|
152
169
|
)
|
|
153
170
|
)
|
|
154
171
|
except (TypeError, ValueError) as error:
|
|
@@ -160,7 +177,7 @@ class WorkerDispatcher:
|
|
|
160
177
|
self,
|
|
161
178
|
request: pb.InvokeExecuteMethodRequest,
|
|
162
179
|
flow: _RegisteredFlow,
|
|
163
|
-
) -> pb.InvokeExecuteMethodResponse:
|
|
180
|
+
) -> Generator[pb.InvokeExecuteMethodOutput, None, pb.InvokeExecuteMethodResponse]:
|
|
164
181
|
if request.HasField("step_input"):
|
|
165
182
|
raise InvalidStepResultError(
|
|
166
183
|
flow.name, _TIMEOUT_HANDLER_STEP_TYPE, "execute", "input must be absent"
|
|
@@ -183,8 +200,29 @@ class WorkerDispatcher:
|
|
|
183
200
|
request.attributes,
|
|
184
201
|
request.step_exe_locals,
|
|
185
202
|
condition_results,
|
|
203
|
+
channel_infos=dict(request.channel_infos),
|
|
204
|
+
loaded_channel_messages=dict(request.loaded_channel_messages),
|
|
205
|
+
loaded_attribute_map_instances=request.loaded_attribute_map_instances,
|
|
206
|
+
loaded_channel_names=request.loaded_channel_names,
|
|
207
|
+
loaded_channel_map_instances=request.loaded_channel_map_instances,
|
|
186
208
|
)
|
|
187
209
|
decision = flow.flow.handle_timeout(context)
|
|
210
|
+
if isgenerator(decision):
|
|
211
|
+
while True:
|
|
212
|
+
try:
|
|
213
|
+
output = next(decision)
|
|
214
|
+
except StopIteration as completion:
|
|
215
|
+
decision = completion.value
|
|
216
|
+
break
|
|
217
|
+
try:
|
|
218
|
+
yield self._map_execute_output(output)
|
|
219
|
+
except (TypeError, ValueError) as error:
|
|
220
|
+
raise InvalidStepResultError(
|
|
221
|
+
flow.name,
|
|
222
|
+
_TIMEOUT_HANDLER_STEP_TYPE,
|
|
223
|
+
"execute",
|
|
224
|
+
str(error),
|
|
225
|
+
) from error
|
|
188
226
|
try:
|
|
189
227
|
if isawaitable(decision):
|
|
190
228
|
raise TypeError(
|
|
@@ -198,6 +236,7 @@ class WorkerDispatcher:
|
|
|
198
236
|
record_events=context.events,
|
|
199
237
|
upsert_step_exe_locals=list(context.local_writes.values()),
|
|
200
238
|
publish_to_channel=context.publications,
|
|
239
|
+
delete_from_channel=context.channel_deletions,
|
|
201
240
|
)
|
|
202
241
|
except (TypeError, ValueError) as error:
|
|
203
242
|
raise InvalidStepResultError(
|
|
@@ -402,8 +441,160 @@ class WorkerDispatcher:
|
|
|
402
441
|
mapped.execute_failure_proceed_step_options.skip_wait_for = (
|
|
403
442
|
target.skips_wait_for
|
|
404
443
|
)
|
|
444
|
+
wait_for_loads = self._map_handler_state_loads(
|
|
445
|
+
flow,
|
|
446
|
+
"WaitFor",
|
|
447
|
+
options.wait_for_load_attribute_maps,
|
|
448
|
+
options.wait_for_load_attribute_map_instances,
|
|
449
|
+
options.wait_for_load_channels,
|
|
450
|
+
options.wait_for_load_channel_maps,
|
|
451
|
+
options.wait_for_load_channel_map_instances,
|
|
452
|
+
)
|
|
453
|
+
mapped.wait_for_load_attribute_map_instances.extend(wait_for_loads[0])
|
|
454
|
+
mapped.wait_for_load_channel_names.extend(wait_for_loads[1])
|
|
455
|
+
mapped.wait_for_load_channel_map_instances.extend(wait_for_loads[2])
|
|
456
|
+
execute_loads = self._map_handler_state_loads(
|
|
457
|
+
flow,
|
|
458
|
+
"Execute",
|
|
459
|
+
options.execute_load_attribute_maps,
|
|
460
|
+
options.execute_load_attribute_map_instances,
|
|
461
|
+
options.execute_load_channels,
|
|
462
|
+
options.execute_load_channel_maps,
|
|
463
|
+
options.execute_load_channel_map_instances,
|
|
464
|
+
)
|
|
465
|
+
mapped.execute_load_attribute_map_instances.extend(execute_loads[0])
|
|
466
|
+
mapped.execute_load_channel_names.extend(execute_loads[1])
|
|
467
|
+
mapped.execute_load_channel_map_instances.extend(execute_loads[2])
|
|
405
468
|
return mapped
|
|
406
469
|
|
|
470
|
+
def map_flow_timeout_handler_options(
|
|
471
|
+
self,
|
|
472
|
+
flow: _RegisteredFlow,
|
|
473
|
+
timeout: timedelta | None,
|
|
474
|
+
policy: FlowTimeoutPolicy,
|
|
475
|
+
options: FlowTimeoutHandlerOptions | None,
|
|
476
|
+
) -> pb.FlowTimeoutHandlerOptions | None:
|
|
477
|
+
"""Map validated timeout-handler options for a Flow start."""
|
|
478
|
+
if options is None:
|
|
479
|
+
return None
|
|
480
|
+
if (
|
|
481
|
+
timeout is None
|
|
482
|
+
or timeout.total_seconds() <= 0
|
|
483
|
+
or policy is not FlowTimeoutPolicy.HANDLER
|
|
484
|
+
):
|
|
485
|
+
raise ValueError(
|
|
486
|
+
"timeout handler options require a positive timeout with HANDLER policy"
|
|
487
|
+
)
|
|
488
|
+
mapped = pb.FlowTimeoutHandlerOptions(
|
|
489
|
+
durability_override=cast(Any, self._map_durability(options.durability)),
|
|
490
|
+
lock_attribute_keys=[
|
|
491
|
+
self._map_lock(lock) for lock in options.lock_attributes
|
|
492
|
+
],
|
|
493
|
+
)
|
|
494
|
+
if options.method_timeout is not None:
|
|
495
|
+
mapped.method_timeout_seconds = self._seconds32(options.method_timeout)
|
|
496
|
+
if options.heartbeat_timeout is not None:
|
|
497
|
+
mapped.heartbeat_timeout_seconds = self._seconds32(
|
|
498
|
+
options.heartbeat_timeout
|
|
499
|
+
)
|
|
500
|
+
if options.retry is not None:
|
|
501
|
+
mapped.retry_policy.CopyFrom(self._map_retry(options.retry))
|
|
502
|
+
state_loads = self._map_handler_state_loads(
|
|
503
|
+
flow,
|
|
504
|
+
"timeout handler",
|
|
505
|
+
options.load_attribute_maps,
|
|
506
|
+
options.load_attribute_map_instances,
|
|
507
|
+
options.load_channels,
|
|
508
|
+
options.load_channel_maps,
|
|
509
|
+
options.load_channel_map_instances,
|
|
510
|
+
)
|
|
511
|
+
mapped.load_attribute_map_instances.extend(state_loads[0])
|
|
512
|
+
mapped.load_channel_names.extend(state_loads[1])
|
|
513
|
+
mapped.load_channel_map_instances.extend(state_loads[2])
|
|
514
|
+
if options._failure_target is not None:
|
|
515
|
+
target = self._registered_movement_target(flow, options._failure_target)
|
|
516
|
+
if target.input_type not in (None, type(None)):
|
|
517
|
+
raise ValueError("timeout handler failure Step must use None input")
|
|
518
|
+
mapped.failure_policy = (
|
|
519
|
+
pb.EXECUTE_METHOD_FAILURE_POLICY_PROCEED_TO_CONFIGURED_STEP
|
|
520
|
+
)
|
|
521
|
+
mapped.failure_proceed_step_type = target.name
|
|
522
|
+
target_options = self.map_step_options(
|
|
523
|
+
flow,
|
|
524
|
+
(
|
|
525
|
+
options._failure_options
|
|
526
|
+
if options._failure_options is not None
|
|
527
|
+
else target.step.get_step_options()
|
|
528
|
+
),
|
|
529
|
+
)
|
|
530
|
+
if target_options is not None:
|
|
531
|
+
mapped.failure_proceed_step_options.CopyFrom(target_options)
|
|
532
|
+
mapped.failure_proceed_step_options.skip_wait_for = target.skips_wait_for
|
|
533
|
+
return mapped
|
|
534
|
+
|
|
535
|
+
@staticmethod
|
|
536
|
+
def _map_handler_state_loads(
|
|
537
|
+
flow: _RegisteredFlow,
|
|
538
|
+
source: str,
|
|
539
|
+
attribute_maps: tuple[AttributeMap[Any], ...],
|
|
540
|
+
attribute_map_instances: tuple[AttributeMapLoad, ...],
|
|
541
|
+
channels: tuple[Channel[Any], ...],
|
|
542
|
+
channel_maps: tuple[ChannelMap[Any], ...],
|
|
543
|
+
channel_map_instances: tuple[ChannelMapLoad, ...],
|
|
544
|
+
) -> tuple[tuple[str, ...], tuple[str, ...], tuple[str, ...]]:
|
|
545
|
+
mapped_attribute_maps: list[str] = []
|
|
546
|
+
for attribute_map in attribute_maps:
|
|
547
|
+
WorkerDispatcher._require_state_load(
|
|
548
|
+
flow, source, attribute_map, AttributeMap
|
|
549
|
+
)
|
|
550
|
+
mapped_attribute_maps.append(f"{attribute_map.name}/")
|
|
551
|
+
for attribute_load in attribute_map_instances:
|
|
552
|
+
if not isinstance(attribute_load, AttributeMapLoad):
|
|
553
|
+
raise TypeError(f"{source} has an invalid AttributeMap instance load")
|
|
554
|
+
WorkerDispatcher._require_state_load(
|
|
555
|
+
flow, source, attribute_load.attribute_map, AttributeMap
|
|
556
|
+
)
|
|
557
|
+
mapped_attribute_maps.append(attribute_load.physical_name)
|
|
558
|
+
mapped_channels: list[str] = []
|
|
559
|
+
for channel in channels:
|
|
560
|
+
WorkerDispatcher._require_state_load(flow, source, channel, Channel)
|
|
561
|
+
mapped_channels.append(channel.name)
|
|
562
|
+
mapped_channel_maps: list[str] = []
|
|
563
|
+
for channel_map in channel_maps:
|
|
564
|
+
WorkerDispatcher._require_state_load(flow, source, channel_map, ChannelMap)
|
|
565
|
+
mapped_channel_maps.append(f"{channel_map.name}/")
|
|
566
|
+
for channel_load in channel_map_instances:
|
|
567
|
+
if not isinstance(channel_load, ChannelMapLoad):
|
|
568
|
+
raise TypeError(f"{source} has an invalid ChannelMap instance load")
|
|
569
|
+
WorkerDispatcher._require_state_load(
|
|
570
|
+
flow, source, channel_load.channel_map, ChannelMap
|
|
571
|
+
)
|
|
572
|
+
mapped_channel_maps.append(channel_load.physical_name)
|
|
573
|
+
for kind, values in (
|
|
574
|
+
("AttributeMap", mapped_attribute_maps),
|
|
575
|
+
("Channel", mapped_channels),
|
|
576
|
+
("ChannelMap", mapped_channel_maps),
|
|
577
|
+
):
|
|
578
|
+
if len(values) != len(set(values)):
|
|
579
|
+
raise ValueError(f"{source} has a duplicate {kind} load")
|
|
580
|
+
return (
|
|
581
|
+
tuple(sorted(mapped_attribute_maps)),
|
|
582
|
+
tuple(sorted(mapped_channels)),
|
|
583
|
+
tuple(sorted(mapped_channel_maps)),
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
@staticmethod
|
|
587
|
+
def _require_state_load(
|
|
588
|
+
flow: _RegisteredFlow,
|
|
589
|
+
source: str,
|
|
590
|
+
definition: object,
|
|
591
|
+
expected_type: type[object],
|
|
592
|
+
) -> None:
|
|
593
|
+
if not isinstance(definition, expected_type):
|
|
594
|
+
raise TypeError(f"{source} has an invalid state load")
|
|
595
|
+
if flow.persistence.get(cast(Any, definition).name) is not definition:
|
|
596
|
+
raise ValueError(f"{source} state load does not belong to Flow {flow.name}")
|
|
597
|
+
|
|
407
598
|
def _map_wait(
|
|
408
599
|
self,
|
|
409
600
|
flow: _RegisteredFlow,
|
|
@@ -467,6 +658,14 @@ class WorkerDispatcher:
|
|
|
467
658
|
FlowTimeoutPolicy.CANCEL: pb.FLOW_TIMEOUT_POLICY_CANCEL,
|
|
468
659
|
FlowTimeoutPolicy.HANDLER: pb.FLOW_TIMEOUT_POLICY_HANDLER,
|
|
469
660
|
}[timeout_policy]
|
|
661
|
+
timeout_handler_options = self.map_flow_timeout_handler_options(
|
|
662
|
+
target,
|
|
663
|
+
options.timeout,
|
|
664
|
+
timeout_policy,
|
|
665
|
+
options.timeout_handler_options,
|
|
666
|
+
)
|
|
667
|
+
if timeout_handler_options is not None:
|
|
668
|
+
mapped.timeout_handler_options.CopyFrom(timeout_handler_options)
|
|
470
669
|
if options.start_delay is not None:
|
|
471
670
|
mapped.flow_start_delay_seconds = self._seconds32(options.start_delay)
|
|
472
671
|
if options.retry_policy is not None:
|