pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.10__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.
- pyworkflow/__init__.py +10 -1
- pyworkflow/celery/tasks.py +272 -24
- pyworkflow/cli/__init__.py +4 -1
- pyworkflow/cli/commands/runs.py +4 -4
- pyworkflow/cli/commands/setup.py +203 -4
- pyworkflow/cli/utils/config_generator.py +76 -3
- pyworkflow/cli/utils/docker_manager.py +232 -0
- pyworkflow/config.py +94 -17
- pyworkflow/context/__init__.py +13 -0
- pyworkflow/context/base.py +26 -0
- pyworkflow/context/local.py +80 -0
- pyworkflow/context/step_context.py +295 -0
- pyworkflow/core/registry.py +6 -1
- pyworkflow/core/step.py +141 -0
- pyworkflow/core/workflow.py +56 -0
- pyworkflow/engine/events.py +30 -0
- pyworkflow/engine/replay.py +39 -0
- pyworkflow/primitives/child_workflow.py +1 -1
- pyworkflow/runtime/local.py +1 -1
- pyworkflow/storage/__init__.py +14 -0
- pyworkflow/storage/base.py +35 -0
- pyworkflow/storage/cassandra.py +1747 -0
- pyworkflow/storage/config.py +69 -0
- pyworkflow/storage/dynamodb.py +31 -2
- pyworkflow/storage/file.py +28 -0
- pyworkflow/storage/memory.py +18 -0
- pyworkflow/storage/mysql.py +1159 -0
- pyworkflow/storage/postgres.py +27 -2
- pyworkflow/storage/schemas.py +4 -3
- pyworkflow/storage/sqlite.py +25 -2
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/METADATA +7 -4
- pyworkflow_engine-0.1.10.dist-info/RECORD +91 -0
- pyworkflow_engine-0.1.10.dist-info/top_level.txt +1 -0
- dashboard/backend/app/__init__.py +0 -1
- dashboard/backend/app/config.py +0 -32
- dashboard/backend/app/controllers/__init__.py +0 -6
- dashboard/backend/app/controllers/run_controller.py +0 -86
- dashboard/backend/app/controllers/workflow_controller.py +0 -33
- dashboard/backend/app/dependencies/__init__.py +0 -5
- dashboard/backend/app/dependencies/storage.py +0 -50
- dashboard/backend/app/repositories/__init__.py +0 -6
- dashboard/backend/app/repositories/run_repository.py +0 -80
- dashboard/backend/app/repositories/workflow_repository.py +0 -27
- dashboard/backend/app/rest/__init__.py +0 -8
- dashboard/backend/app/rest/v1/__init__.py +0 -12
- dashboard/backend/app/rest/v1/health.py +0 -33
- dashboard/backend/app/rest/v1/runs.py +0 -133
- dashboard/backend/app/rest/v1/workflows.py +0 -41
- dashboard/backend/app/schemas/__init__.py +0 -23
- dashboard/backend/app/schemas/common.py +0 -16
- dashboard/backend/app/schemas/event.py +0 -24
- dashboard/backend/app/schemas/hook.py +0 -25
- dashboard/backend/app/schemas/run.py +0 -54
- dashboard/backend/app/schemas/step.py +0 -28
- dashboard/backend/app/schemas/workflow.py +0 -31
- dashboard/backend/app/server.py +0 -87
- dashboard/backend/app/services/__init__.py +0 -6
- dashboard/backend/app/services/run_service.py +0 -240
- dashboard/backend/app/services/workflow_service.py +0 -155
- dashboard/backend/main.py +0 -18
- docs/concepts/cancellation.mdx +0 -362
- docs/concepts/continue-as-new.mdx +0 -434
- docs/concepts/events.mdx +0 -266
- docs/concepts/fault-tolerance.mdx +0 -370
- docs/concepts/hooks.mdx +0 -552
- docs/concepts/limitations.mdx +0 -167
- docs/concepts/schedules.mdx +0 -775
- docs/concepts/sleep.mdx +0 -312
- docs/concepts/steps.mdx +0 -301
- docs/concepts/workflows.mdx +0 -255
- docs/guides/cli.mdx +0 -942
- docs/guides/configuration.mdx +0 -560
- docs/introduction.mdx +0 -155
- docs/quickstart.mdx +0 -279
- examples/__init__.py +0 -1
- examples/celery/__init__.py +0 -1
- examples/celery/durable/docker-compose.yml +0 -55
- examples/celery/durable/pyworkflow.config.yaml +0 -12
- examples/celery/durable/workflows/__init__.py +0 -122
- examples/celery/durable/workflows/basic.py +0 -87
- examples/celery/durable/workflows/batch_processing.py +0 -102
- examples/celery/durable/workflows/cancellation.py +0 -273
- examples/celery/durable/workflows/child_workflow_patterns.py +0 -240
- examples/celery/durable/workflows/child_workflows.py +0 -202
- examples/celery/durable/workflows/continue_as_new.py +0 -260
- examples/celery/durable/workflows/fault_tolerance.py +0 -210
- examples/celery/durable/workflows/hooks.py +0 -211
- examples/celery/durable/workflows/idempotency.py +0 -112
- examples/celery/durable/workflows/long_running.py +0 -99
- examples/celery/durable/workflows/retries.py +0 -101
- examples/celery/durable/workflows/schedules.py +0 -209
- examples/celery/transient/01_basic_workflow.py +0 -91
- examples/celery/transient/02_fault_tolerance.py +0 -257
- examples/celery/transient/__init__.py +0 -20
- examples/celery/transient/pyworkflow.config.yaml +0 -25
- examples/local/__init__.py +0 -1
- examples/local/durable/01_basic_workflow.py +0 -94
- examples/local/durable/02_file_storage.py +0 -132
- examples/local/durable/03_retries.py +0 -169
- examples/local/durable/04_long_running.py +0 -119
- examples/local/durable/05_event_log.py +0 -145
- examples/local/durable/06_idempotency.py +0 -148
- examples/local/durable/07_hooks.py +0 -334
- examples/local/durable/08_cancellation.py +0 -233
- examples/local/durable/09_child_workflows.py +0 -198
- examples/local/durable/10_child_workflow_patterns.py +0 -265
- examples/local/durable/11_continue_as_new.py +0 -249
- examples/local/durable/12_schedules.py +0 -198
- examples/local/durable/__init__.py +0 -1
- examples/local/transient/01_quick_tasks.py +0 -87
- examples/local/transient/02_retries.py +0 -130
- examples/local/transient/03_sleep.py +0 -141
- examples/local/transient/__init__.py +0 -1
- pyworkflow_engine-0.1.7.dist-info/RECORD +0 -196
- pyworkflow_engine-0.1.7.dist-info/top_level.txt +0 -5
- tests/examples/__init__.py +0 -0
- tests/integration/__init__.py +0 -0
- tests/integration/test_cancellation.py +0 -330
- tests/integration/test_child_workflows.py +0 -439
- tests/integration/test_continue_as_new.py +0 -428
- tests/integration/test_dynamodb_storage.py +0 -1146
- tests/integration/test_fault_tolerance.py +0 -369
- tests/integration/test_schedule_storage.py +0 -484
- tests/unit/__init__.py +0 -0
- tests/unit/backends/__init__.py +0 -1
- tests/unit/backends/test_dynamodb_storage.py +0 -1554
- tests/unit/backends/test_postgres_storage.py +0 -1281
- tests/unit/backends/test_sqlite_storage.py +0 -1460
- tests/unit/conftest.py +0 -41
- tests/unit/test_cancellation.py +0 -364
- tests/unit/test_child_workflows.py +0 -680
- tests/unit/test_continue_as_new.py +0 -441
- tests/unit/test_event_limits.py +0 -316
- tests/unit/test_executor.py +0 -320
- tests/unit/test_fault_tolerance.py +0 -334
- tests/unit/test_hooks.py +0 -495
- tests/unit/test_registry.py +0 -261
- tests/unit/test_replay.py +0 -420
- tests/unit/test_schedule_schemas.py +0 -285
- tests/unit/test_schedule_utils.py +0 -286
- tests/unit/test_scheduled_workflow.py +0 -274
- tests/unit/test_step.py +0 -353
- tests/unit/test_workflow.py +0 -243
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/WHEEL +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/entry_points.txt +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.10.dist-info}/licenses/LICENSE +0 -0
pyworkflow/__init__.py
CHANGED
|
@@ -29,7 +29,7 @@ Quick Start:
|
|
|
29
29
|
>>> run_id = await start(my_workflow, "Alice")
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
|
-
__version__ = "0.1.
|
|
32
|
+
__version__ = "0.1.10"
|
|
33
33
|
|
|
34
34
|
# Configuration
|
|
35
35
|
from pyworkflow.config import (
|
|
@@ -44,11 +44,15 @@ from pyworkflow.config import (
|
|
|
44
44
|
from pyworkflow.context import (
|
|
45
45
|
LocalContext,
|
|
46
46
|
MockContext,
|
|
47
|
+
StepContext,
|
|
47
48
|
WorkflowContext,
|
|
48
49
|
get_context,
|
|
50
|
+
get_step_context,
|
|
49
51
|
has_context,
|
|
52
|
+
has_step_context,
|
|
50
53
|
reset_context,
|
|
51
54
|
set_context,
|
|
55
|
+
set_step_context,
|
|
52
56
|
)
|
|
53
57
|
|
|
54
58
|
# Exceptions
|
|
@@ -224,6 +228,11 @@ __all__ = [
|
|
|
224
228
|
"has_context",
|
|
225
229
|
"set_context",
|
|
226
230
|
"reset_context",
|
|
231
|
+
# Step context for distributed execution
|
|
232
|
+
"StepContext",
|
|
233
|
+
"get_step_context",
|
|
234
|
+
"has_step_context",
|
|
235
|
+
"set_step_context",
|
|
227
236
|
# Registry
|
|
228
237
|
"list_workflows",
|
|
229
238
|
"get_workflow",
|
pyworkflow/celery/tasks.py
CHANGED
|
@@ -13,7 +13,10 @@ import asyncio
|
|
|
13
13
|
import uuid
|
|
14
14
|
from collections.abc import Callable
|
|
15
15
|
from datetime import UTC, datetime
|
|
16
|
-
from typing import Any
|
|
16
|
+
from typing import TYPE_CHECKING, Any
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from pyworkflow.context.step_context import StepContext
|
|
17
20
|
|
|
18
21
|
from celery import Task
|
|
19
22
|
from celery.exceptions import WorkerLostError
|
|
@@ -103,11 +106,16 @@ def execute_step_task(
|
|
|
103
106
|
step_id: str,
|
|
104
107
|
max_retries: int = 3,
|
|
105
108
|
storage_config: dict[str, Any] | None = None,
|
|
109
|
+
context_data: dict[str, Any] | None = None,
|
|
110
|
+
context_class_name: str | None = None,
|
|
106
111
|
) -> Any:
|
|
107
112
|
"""
|
|
108
|
-
Execute a workflow step
|
|
113
|
+
Execute a workflow step on a Celery worker.
|
|
109
114
|
|
|
110
|
-
This task
|
|
115
|
+
This task:
|
|
116
|
+
1. Executes the step function
|
|
117
|
+
2. Records STEP_COMPLETED/STEP_FAILED event in storage
|
|
118
|
+
3. Triggers workflow resumption via resume_workflow_task
|
|
111
119
|
|
|
112
120
|
Args:
|
|
113
121
|
step_name: Name of the step function
|
|
@@ -117,6 +125,8 @@ def execute_step_task(
|
|
|
117
125
|
step_id: Step execution ID
|
|
118
126
|
max_retries: Maximum retry attempts
|
|
119
127
|
storage_config: Storage backend configuration
|
|
128
|
+
context_data: Optional step context data (from workflow)
|
|
129
|
+
context_class_name: Optional fully qualified context class name
|
|
120
130
|
|
|
121
131
|
Returns:
|
|
122
132
|
Step result (serialized)
|
|
@@ -128,7 +138,7 @@ def execute_step_task(
|
|
|
128
138
|
from pyworkflow.core.registry import _registry
|
|
129
139
|
|
|
130
140
|
logger.info(
|
|
131
|
-
f"Executing step: {step_name}",
|
|
141
|
+
f"Executing dispatched step: {step_name}",
|
|
132
142
|
run_id=run_id,
|
|
133
143
|
step_id=step_id,
|
|
134
144
|
attempt=self.request.retries + 1,
|
|
@@ -137,12 +147,49 @@ def execute_step_task(
|
|
|
137
147
|
# Get step metadata
|
|
138
148
|
step_meta = _registry.get_step(step_name)
|
|
139
149
|
if not step_meta:
|
|
150
|
+
# Record failure and resume workflow
|
|
151
|
+
asyncio.run(
|
|
152
|
+
_record_step_failure_and_resume(
|
|
153
|
+
storage_config=storage_config,
|
|
154
|
+
run_id=run_id,
|
|
155
|
+
step_id=step_id,
|
|
156
|
+
step_name=step_name,
|
|
157
|
+
error=f"Step '{step_name}' not found in registry",
|
|
158
|
+
error_type="FatalError",
|
|
159
|
+
is_retryable=False,
|
|
160
|
+
)
|
|
161
|
+
)
|
|
140
162
|
raise FatalError(f"Step '{step_name}' not found in registry")
|
|
141
163
|
|
|
142
164
|
# Deserialize arguments
|
|
143
165
|
args = deserialize_args(args_json)
|
|
144
166
|
kwargs = deserialize_kwargs(kwargs_json)
|
|
145
167
|
|
|
168
|
+
# Set up step context if provided (read-only mode)
|
|
169
|
+
step_context_token = None
|
|
170
|
+
readonly_token = None
|
|
171
|
+
|
|
172
|
+
if context_data and context_class_name:
|
|
173
|
+
try:
|
|
174
|
+
from pyworkflow.context.step_context import (
|
|
175
|
+
_set_step_context_internal,
|
|
176
|
+
_set_step_context_readonly,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Import context class dynamically
|
|
180
|
+
context_class = _resolve_context_class(context_class_name)
|
|
181
|
+
if context_class is not None:
|
|
182
|
+
step_ctx = context_class.from_dict(context_data)
|
|
183
|
+
step_context_token = _set_step_context_internal(step_ctx)
|
|
184
|
+
# Set readonly mode to prevent mutation in steps
|
|
185
|
+
readonly_token = _set_step_context_readonly(True)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.warning(
|
|
188
|
+
f"Failed to load step context: {e}",
|
|
189
|
+
run_id=run_id,
|
|
190
|
+
step_id=step_id,
|
|
191
|
+
)
|
|
192
|
+
|
|
146
193
|
# Execute step function
|
|
147
194
|
try:
|
|
148
195
|
# Get the original function (unwrapped from decorator)
|
|
@@ -160,32 +207,225 @@ def execute_step_task(
|
|
|
160
207
|
step_id=step_id,
|
|
161
208
|
)
|
|
162
209
|
|
|
210
|
+
# Record STEP_COMPLETED event and trigger workflow resumption
|
|
211
|
+
asyncio.run(
|
|
212
|
+
_record_step_completion_and_resume(
|
|
213
|
+
storage_config=storage_config,
|
|
214
|
+
run_id=run_id,
|
|
215
|
+
step_id=step_id,
|
|
216
|
+
step_name=step_name,
|
|
217
|
+
result=result,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
163
221
|
return result
|
|
164
222
|
|
|
165
|
-
except FatalError:
|
|
223
|
+
except FatalError as e:
|
|
166
224
|
logger.error(f"Step failed (fatal): {step_name}", run_id=run_id, step_id=step_id)
|
|
225
|
+
# Record failure and resume workflow (workflow will fail on replay)
|
|
226
|
+
asyncio.run(
|
|
227
|
+
_record_step_failure_and_resume(
|
|
228
|
+
storage_config=storage_config,
|
|
229
|
+
run_id=run_id,
|
|
230
|
+
step_id=step_id,
|
|
231
|
+
step_name=step_name,
|
|
232
|
+
error=str(e),
|
|
233
|
+
error_type=type(e).__name__,
|
|
234
|
+
is_retryable=False,
|
|
235
|
+
)
|
|
236
|
+
)
|
|
167
237
|
raise
|
|
168
238
|
|
|
169
239
|
except RetryableError as e:
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
240
|
+
# Check if we have retries left
|
|
241
|
+
if self.request.retries < max_retries:
|
|
242
|
+
logger.warning(
|
|
243
|
+
f"Step failed (retriable): {step_name}, retrying...",
|
|
244
|
+
run_id=run_id,
|
|
245
|
+
step_id=step_id,
|
|
246
|
+
retry_after=e.retry_after,
|
|
247
|
+
attempt=self.request.retries + 1,
|
|
248
|
+
max_retries=max_retries,
|
|
249
|
+
)
|
|
250
|
+
# Let Celery handle the retry - don't resume workflow yet
|
|
251
|
+
raise self.retry(exc=e, countdown=e.get_retry_delay_seconds() or 60)
|
|
252
|
+
else:
|
|
253
|
+
# Max retries exhausted - record failure and resume workflow
|
|
254
|
+
logger.error(
|
|
255
|
+
f"Step failed after {max_retries + 1} attempts: {step_name}",
|
|
256
|
+
run_id=run_id,
|
|
257
|
+
step_id=step_id,
|
|
258
|
+
)
|
|
259
|
+
asyncio.run(
|
|
260
|
+
_record_step_failure_and_resume(
|
|
261
|
+
storage_config=storage_config,
|
|
262
|
+
run_id=run_id,
|
|
263
|
+
step_id=step_id,
|
|
264
|
+
step_name=step_name,
|
|
265
|
+
error=str(e),
|
|
266
|
+
error_type=type(e).__name__,
|
|
267
|
+
is_retryable=False, # Mark as not retryable since we exhausted retries
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
raise
|
|
178
271
|
|
|
179
272
|
except Exception as e:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
273
|
+
# Check if we have retries left
|
|
274
|
+
if self.request.retries < max_retries:
|
|
275
|
+
logger.warning(
|
|
276
|
+
f"Step failed (unexpected): {step_name}, retrying...",
|
|
277
|
+
run_id=run_id,
|
|
278
|
+
step_id=step_id,
|
|
279
|
+
error=str(e),
|
|
280
|
+
attempt=self.request.retries + 1,
|
|
281
|
+
)
|
|
282
|
+
# Treat unexpected errors as retriable
|
|
283
|
+
raise self.retry(exc=RetryableError(str(e)), countdown=60)
|
|
284
|
+
else:
|
|
285
|
+
# Max retries exhausted
|
|
286
|
+
logger.error(
|
|
287
|
+
f"Step failed after {max_retries + 1} attempts: {step_name}",
|
|
288
|
+
run_id=run_id,
|
|
289
|
+
step_id=step_id,
|
|
290
|
+
error=str(e),
|
|
291
|
+
exc_info=True,
|
|
292
|
+
)
|
|
293
|
+
asyncio.run(
|
|
294
|
+
_record_step_failure_and_resume(
|
|
295
|
+
storage_config=storage_config,
|
|
296
|
+
run_id=run_id,
|
|
297
|
+
step_id=step_id,
|
|
298
|
+
step_name=step_name,
|
|
299
|
+
error=str(e),
|
|
300
|
+
error_type=type(e).__name__,
|
|
301
|
+
is_retryable=False,
|
|
302
|
+
)
|
|
303
|
+
)
|
|
304
|
+
raise
|
|
305
|
+
|
|
306
|
+
finally:
|
|
307
|
+
# Clean up step context
|
|
308
|
+
if readonly_token is not None:
|
|
309
|
+
from pyworkflow.context.step_context import _reset_step_context_readonly
|
|
310
|
+
|
|
311
|
+
_reset_step_context_readonly(readonly_token)
|
|
312
|
+
if step_context_token is not None:
|
|
313
|
+
from pyworkflow.context.step_context import _reset_step_context
|
|
314
|
+
|
|
315
|
+
_reset_step_context(step_context_token)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
async def _record_step_completion_and_resume(
|
|
319
|
+
storage_config: dict[str, Any] | None,
|
|
320
|
+
run_id: str,
|
|
321
|
+
step_id: str,
|
|
322
|
+
step_name: str,
|
|
323
|
+
result: Any,
|
|
324
|
+
) -> None:
|
|
325
|
+
"""
|
|
326
|
+
Record STEP_COMPLETED event and trigger workflow resumption.
|
|
327
|
+
|
|
328
|
+
Called by execute_step_task after successful step execution.
|
|
329
|
+
"""
|
|
330
|
+
from pyworkflow.engine.events import create_step_completed_event
|
|
331
|
+
from pyworkflow.serialization.encoder import serialize
|
|
332
|
+
|
|
333
|
+
# Get storage backend
|
|
334
|
+
storage = _get_storage_backend(storage_config)
|
|
335
|
+
|
|
336
|
+
# Ensure storage is connected
|
|
337
|
+
if hasattr(storage, "connect"):
|
|
338
|
+
await storage.connect()
|
|
339
|
+
|
|
340
|
+
# Record STEP_COMPLETED event
|
|
341
|
+
completion_event = create_step_completed_event(
|
|
342
|
+
run_id=run_id,
|
|
343
|
+
step_id=step_id,
|
|
344
|
+
result=serialize(result),
|
|
345
|
+
step_name=step_name,
|
|
346
|
+
)
|
|
347
|
+
await storage.record_event(completion_event)
|
|
348
|
+
|
|
349
|
+
# Schedule workflow resumption immediately
|
|
350
|
+
schedule_workflow_resumption(run_id, datetime.now(UTC), storage_config)
|
|
351
|
+
|
|
352
|
+
logger.info(
|
|
353
|
+
"Step completed and workflow resumption scheduled",
|
|
354
|
+
run_id=run_id,
|
|
355
|
+
step_id=step_id,
|
|
356
|
+
step_name=step_name,
|
|
357
|
+
)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def _record_step_failure_and_resume(
|
|
361
|
+
storage_config: dict[str, Any] | None,
|
|
362
|
+
run_id: str,
|
|
363
|
+
step_id: str,
|
|
364
|
+
step_name: str,
|
|
365
|
+
error: str,
|
|
366
|
+
error_type: str,
|
|
367
|
+
is_retryable: bool,
|
|
368
|
+
) -> None:
|
|
369
|
+
"""
|
|
370
|
+
Record STEP_FAILED event and trigger workflow resumption.
|
|
371
|
+
|
|
372
|
+
Called by execute_step_task after step failure (when retries are exhausted).
|
|
373
|
+
The workflow will fail when it replays and sees the failure event.
|
|
374
|
+
"""
|
|
375
|
+
from pyworkflow.engine.events import create_step_failed_event
|
|
376
|
+
|
|
377
|
+
# Get storage backend
|
|
378
|
+
storage = _get_storage_backend(storage_config)
|
|
379
|
+
|
|
380
|
+
# Ensure storage is connected
|
|
381
|
+
if hasattr(storage, "connect"):
|
|
382
|
+
await storage.connect()
|
|
383
|
+
|
|
384
|
+
# Record STEP_FAILED event
|
|
385
|
+
failure_event = create_step_failed_event(
|
|
386
|
+
run_id=run_id,
|
|
387
|
+
step_id=step_id,
|
|
388
|
+
error=error,
|
|
389
|
+
error_type=error_type,
|
|
390
|
+
is_retryable=is_retryable,
|
|
391
|
+
attempt=1, # Final attempt
|
|
392
|
+
)
|
|
393
|
+
await storage.record_event(failure_event)
|
|
394
|
+
|
|
395
|
+
# Schedule workflow resumption - workflow will fail on replay
|
|
396
|
+
schedule_workflow_resumption(run_id, datetime.now(UTC), storage_config)
|
|
397
|
+
|
|
398
|
+
logger.info(
|
|
399
|
+
"Step failed and workflow resumption scheduled",
|
|
400
|
+
run_id=run_id,
|
|
401
|
+
step_id=step_id,
|
|
402
|
+
step_name=step_name,
|
|
403
|
+
error=error,
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _resolve_context_class(class_name: str) -> type["StepContext"] | None:
|
|
408
|
+
"""
|
|
409
|
+
Resolve a context class from its fully qualified name.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
class_name: Fully qualified class name (e.g., "myapp.contexts.OrderContext")
|
|
413
|
+
|
|
414
|
+
Returns:
|
|
415
|
+
The class type, or None if resolution fails
|
|
416
|
+
"""
|
|
417
|
+
try:
|
|
418
|
+
import importlib
|
|
419
|
+
|
|
420
|
+
parts = class_name.rsplit(".", 1)
|
|
421
|
+
if len(parts) == 2:
|
|
422
|
+
module_name, cls_name = parts
|
|
423
|
+
module = importlib.import_module(module_name)
|
|
424
|
+
return getattr(module, cls_name, None)
|
|
425
|
+
# Simple class name - try to get from globals
|
|
426
|
+
return None
|
|
427
|
+
except Exception:
|
|
428
|
+
return None
|
|
189
429
|
|
|
190
430
|
|
|
191
431
|
@celery_app.task(
|
|
@@ -365,6 +605,8 @@ async def _execute_child_workflow_on_worker(
|
|
|
365
605
|
storage=storage,
|
|
366
606
|
durable=True,
|
|
367
607
|
event_log=None, # Fresh execution
|
|
608
|
+
runtime="celery",
|
|
609
|
+
storage_config=storage_config,
|
|
368
610
|
)
|
|
369
611
|
|
|
370
612
|
# Update status to COMPLETED
|
|
@@ -714,6 +956,8 @@ async def _recover_workflow_on_worker(
|
|
|
714
956
|
args=args,
|
|
715
957
|
kwargs=kwargs,
|
|
716
958
|
event_log=events,
|
|
959
|
+
runtime="celery",
|
|
960
|
+
storage_config=storage_config,
|
|
717
961
|
)
|
|
718
962
|
|
|
719
963
|
# Update run status to completed
|
|
@@ -949,7 +1193,7 @@ async def _start_workflow_on_worker(
|
|
|
949
1193
|
input_kwargs=serialize_kwargs(**kwargs),
|
|
950
1194
|
idempotency_key=idempotency_key,
|
|
951
1195
|
max_duration=workflow_meta.max_duration,
|
|
952
|
-
|
|
1196
|
+
context={}, # Step context (not from decorator)
|
|
953
1197
|
recovery_attempts=0,
|
|
954
1198
|
max_recovery_attempts=max_recovery_attempts,
|
|
955
1199
|
recover_on_worker_loss=recover_on_worker_loss,
|
|
@@ -977,6 +1221,8 @@ async def _start_workflow_on_worker(
|
|
|
977
1221
|
storage=storage,
|
|
978
1222
|
args=args,
|
|
979
1223
|
kwargs=kwargs,
|
|
1224
|
+
runtime="celery",
|
|
1225
|
+
storage_config=storage_config,
|
|
980
1226
|
)
|
|
981
1227
|
|
|
982
1228
|
# Update run status to completed
|
|
@@ -1231,7 +1477,7 @@ async def _execute_scheduled_workflow(
|
|
|
1231
1477
|
kwargs_json=kwargs_json,
|
|
1232
1478
|
run_id=run_id,
|
|
1233
1479
|
storage_config=storage_config,
|
|
1234
|
-
|
|
1480
|
+
# Note: context data is passed through for scheduled workflows to include schedule info
|
|
1235
1481
|
)
|
|
1236
1482
|
|
|
1237
1483
|
# Record trigger event - use schedule_id as run_id since workflow run may not exist yet
|
|
@@ -1381,6 +1627,8 @@ async def _resume_workflow_on_worker(
|
|
|
1381
1627
|
kwargs=kwargs,
|
|
1382
1628
|
event_log=events,
|
|
1383
1629
|
cancellation_requested=cancellation_requested,
|
|
1630
|
+
runtime="celery",
|
|
1631
|
+
storage_config=storage_config,
|
|
1384
1632
|
)
|
|
1385
1633
|
|
|
1386
1634
|
# Update run status to completed
|
pyworkflow/cli/__init__.py
CHANGED
|
@@ -27,7 +27,10 @@ from pyworkflow.cli.utils.storage import create_storage
|
|
|
27
27
|
)
|
|
28
28
|
@click.option(
|
|
29
29
|
"--storage",
|
|
30
|
-
type=click.Choice(
|
|
30
|
+
type=click.Choice(
|
|
31
|
+
["file", "memory", "sqlite", "postgres", "mysql", "dynamodb", "cassandra"],
|
|
32
|
+
case_sensitive=False,
|
|
33
|
+
),
|
|
31
34
|
envvar="PYWORKFLOW_STORAGE_BACKEND",
|
|
32
35
|
help="Storage backend type (default: file)",
|
|
33
36
|
)
|
pyworkflow/cli/commands/runs.py
CHANGED
|
@@ -227,7 +227,7 @@ async def run_status(ctx: click.Context, run_id: str) -> None:
|
|
|
227
227
|
"input_kwargs": json.loads(run.input_kwargs) if run.input_kwargs else None,
|
|
228
228
|
"result": json.loads(run.result) if run.result else None,
|
|
229
229
|
"error": run.error,
|
|
230
|
-
"
|
|
230
|
+
"context": run.context,
|
|
231
231
|
}
|
|
232
232
|
format_json(data)
|
|
233
233
|
|
|
@@ -266,9 +266,9 @@ async def run_status(ctx: click.Context, run_id: str) -> None:
|
|
|
266
266
|
if run.error:
|
|
267
267
|
data["Error"] = run.error
|
|
268
268
|
|
|
269
|
-
# Add
|
|
270
|
-
if run.
|
|
271
|
-
data["
|
|
269
|
+
# Add context if present
|
|
270
|
+
if run.context:
|
|
271
|
+
data["Context"] = json.dumps(run.context, indent=2)
|
|
272
272
|
|
|
273
273
|
format_key_value(data, title=f"Workflow Run: {run_id}")
|
|
274
274
|
|