pyworkflow-engine 0.1.7__py3-none-any.whl → 0.1.9__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/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.9.dist-info}/METADATA +7 -4
- pyworkflow_engine-0.1.9.dist-info/RECORD +91 -0
- pyworkflow_engine-0.1.9.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.9.dist-info}/WHEEL +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/entry_points.txt +0 -0
- {pyworkflow_engine-0.1.7.dist-info → pyworkflow_engine-0.1.9.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,334 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Unit tests for fault tolerance features.
|
|
3
|
-
|
|
4
|
-
Tests cover:
|
|
5
|
-
- WORKFLOW_INTERRUPTED event type
|
|
6
|
-
- RunStatus.INTERRUPTED status
|
|
7
|
-
- WorkflowRun recovery tracking fields
|
|
8
|
-
- Replay mechanism handling of WORKFLOW_INTERRUPTED
|
|
9
|
-
- Recovery config options
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from datetime import UTC, datetime
|
|
13
|
-
|
|
14
|
-
import pytest
|
|
15
|
-
|
|
16
|
-
from pyworkflow.config import PyWorkflowConfig
|
|
17
|
-
from pyworkflow.engine.events import (
|
|
18
|
-
Event,
|
|
19
|
-
EventType,
|
|
20
|
-
create_workflow_interrupted_event,
|
|
21
|
-
)
|
|
22
|
-
from pyworkflow.storage.schemas import RunStatus, WorkflowRun
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class TestWorkflowInterruptedEvent:
|
|
26
|
-
"""Tests for WORKFLOW_INTERRUPTED event type."""
|
|
27
|
-
|
|
28
|
-
def test_event_type_exists(self):
|
|
29
|
-
"""WORKFLOW_INTERRUPTED should be defined in EventType."""
|
|
30
|
-
assert hasattr(EventType, "WORKFLOW_INTERRUPTED")
|
|
31
|
-
assert EventType.WORKFLOW_INTERRUPTED.value == "workflow.interrupted"
|
|
32
|
-
|
|
33
|
-
def test_create_workflow_interrupted_event(self):
|
|
34
|
-
"""Should create a valid WORKFLOW_INTERRUPTED event."""
|
|
35
|
-
event = create_workflow_interrupted_event(
|
|
36
|
-
run_id="test_run_123",
|
|
37
|
-
reason="worker_lost",
|
|
38
|
-
worker_id="worker_1",
|
|
39
|
-
last_event_sequence=5,
|
|
40
|
-
error="Worker process terminated unexpectedly",
|
|
41
|
-
recovery_attempt=1,
|
|
42
|
-
recoverable=True,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
assert event.run_id == "test_run_123"
|
|
46
|
-
assert event.type == EventType.WORKFLOW_INTERRUPTED
|
|
47
|
-
assert event.data["reason"] == "worker_lost"
|
|
48
|
-
assert event.data["worker_id"] == "worker_1"
|
|
49
|
-
assert event.data["last_event_sequence"] == 5
|
|
50
|
-
assert event.data["error"] == "Worker process terminated unexpectedly"
|
|
51
|
-
assert event.data["recovery_attempt"] == 1
|
|
52
|
-
assert event.data["recoverable"] is True
|
|
53
|
-
|
|
54
|
-
def test_create_workflow_interrupted_event_minimal(self):
|
|
55
|
-
"""Should create event with minimal required fields."""
|
|
56
|
-
event = create_workflow_interrupted_event(
|
|
57
|
-
run_id="test_run_456",
|
|
58
|
-
reason="timeout",
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
assert event.run_id == "test_run_456"
|
|
62
|
-
assert event.type == EventType.WORKFLOW_INTERRUPTED
|
|
63
|
-
assert event.data["reason"] == "timeout"
|
|
64
|
-
assert event.data["worker_id"] is None
|
|
65
|
-
assert event.data["last_event_sequence"] is None
|
|
66
|
-
assert event.data["error"] is None
|
|
67
|
-
assert event.data["recovery_attempt"] == 1
|
|
68
|
-
assert event.data["recoverable"] is True
|
|
69
|
-
|
|
70
|
-
def test_event_has_event_id(self):
|
|
71
|
-
"""Should generate a unique event_id."""
|
|
72
|
-
event = create_workflow_interrupted_event(
|
|
73
|
-
run_id="test_run",
|
|
74
|
-
reason="signal",
|
|
75
|
-
)
|
|
76
|
-
|
|
77
|
-
assert event.event_id is not None
|
|
78
|
-
assert event.event_id.startswith("evt_")
|
|
79
|
-
|
|
80
|
-
def test_event_has_timestamp(self):
|
|
81
|
-
"""Should have a timestamp."""
|
|
82
|
-
before = datetime.now(UTC)
|
|
83
|
-
event = create_workflow_interrupted_event(
|
|
84
|
-
run_id="test_run",
|
|
85
|
-
reason="worker_lost",
|
|
86
|
-
)
|
|
87
|
-
after = datetime.now(UTC)
|
|
88
|
-
|
|
89
|
-
assert event.timestamp is not None
|
|
90
|
-
assert before <= event.timestamp <= after
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class TestRunStatusInterrupted:
|
|
94
|
-
"""Tests for RunStatus.INTERRUPTED."""
|
|
95
|
-
|
|
96
|
-
def test_status_exists(self):
|
|
97
|
-
"""INTERRUPTED should be defined in RunStatus."""
|
|
98
|
-
assert hasattr(RunStatus, "INTERRUPTED")
|
|
99
|
-
assert RunStatus.INTERRUPTED.value == "interrupted"
|
|
100
|
-
|
|
101
|
-
def test_status_serialization(self):
|
|
102
|
-
"""Status should serialize and deserialize correctly."""
|
|
103
|
-
status = RunStatus.INTERRUPTED
|
|
104
|
-
serialized = status.value
|
|
105
|
-
|
|
106
|
-
assert serialized == "interrupted"
|
|
107
|
-
assert RunStatus(serialized) == RunStatus.INTERRUPTED
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class TestWorkflowRunRecoveryFields:
|
|
111
|
-
"""Tests for WorkflowRun recovery tracking fields."""
|
|
112
|
-
|
|
113
|
-
def test_default_values(self):
|
|
114
|
-
"""Should have correct default values for recovery fields."""
|
|
115
|
-
run = WorkflowRun(
|
|
116
|
-
run_id="test_run",
|
|
117
|
-
workflow_name="test_workflow",
|
|
118
|
-
status=RunStatus.PENDING,
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
assert run.recovery_attempts == 0
|
|
122
|
-
assert run.max_recovery_attempts == 3
|
|
123
|
-
assert run.recover_on_worker_loss is True
|
|
124
|
-
|
|
125
|
-
def test_custom_values(self):
|
|
126
|
-
"""Should accept custom recovery field values."""
|
|
127
|
-
run = WorkflowRun(
|
|
128
|
-
run_id="test_run",
|
|
129
|
-
workflow_name="test_workflow",
|
|
130
|
-
status=RunStatus.PENDING,
|
|
131
|
-
recovery_attempts=2,
|
|
132
|
-
max_recovery_attempts=5,
|
|
133
|
-
recover_on_worker_loss=False,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
assert run.recovery_attempts == 2
|
|
137
|
-
assert run.max_recovery_attempts == 5
|
|
138
|
-
assert run.recover_on_worker_loss is False
|
|
139
|
-
|
|
140
|
-
def test_to_dict_includes_recovery_fields(self):
|
|
141
|
-
"""to_dict() should include recovery fields."""
|
|
142
|
-
run = WorkflowRun(
|
|
143
|
-
run_id="test_run",
|
|
144
|
-
workflow_name="test_workflow",
|
|
145
|
-
status=RunStatus.RUNNING,
|
|
146
|
-
recovery_attempts=1,
|
|
147
|
-
max_recovery_attempts=3,
|
|
148
|
-
recover_on_worker_loss=True,
|
|
149
|
-
)
|
|
150
|
-
|
|
151
|
-
data = run.to_dict()
|
|
152
|
-
|
|
153
|
-
assert data["recovery_attempts"] == 1
|
|
154
|
-
assert data["max_recovery_attempts"] == 3
|
|
155
|
-
assert data["recover_on_worker_loss"] is True
|
|
156
|
-
|
|
157
|
-
def test_from_dict_reads_recovery_fields(self):
|
|
158
|
-
"""from_dict() should read recovery fields."""
|
|
159
|
-
data = {
|
|
160
|
-
"run_id": "test_run",
|
|
161
|
-
"workflow_name": "test_workflow",
|
|
162
|
-
"status": "running",
|
|
163
|
-
"created_at": datetime.now(UTC).isoformat(),
|
|
164
|
-
"updated_at": datetime.now(UTC).isoformat(),
|
|
165
|
-
"recovery_attempts": 2,
|
|
166
|
-
"max_recovery_attempts": 4,
|
|
167
|
-
"recover_on_worker_loss": False,
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
run = WorkflowRun.from_dict(data)
|
|
171
|
-
|
|
172
|
-
assert run.recovery_attempts == 2
|
|
173
|
-
assert run.max_recovery_attempts == 4
|
|
174
|
-
assert run.recover_on_worker_loss is False
|
|
175
|
-
|
|
176
|
-
def test_from_dict_defaults_missing_recovery_fields(self):
|
|
177
|
-
"""from_dict() should use defaults for missing recovery fields."""
|
|
178
|
-
data = {
|
|
179
|
-
"run_id": "test_run",
|
|
180
|
-
"workflow_name": "test_workflow",
|
|
181
|
-
"status": "running",
|
|
182
|
-
"created_at": datetime.now(UTC).isoformat(),
|
|
183
|
-
"updated_at": datetime.now(UTC).isoformat(),
|
|
184
|
-
# No recovery fields
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
run = WorkflowRun.from_dict(data)
|
|
188
|
-
|
|
189
|
-
assert run.recovery_attempts == 0
|
|
190
|
-
assert run.max_recovery_attempts == 3
|
|
191
|
-
assert run.recover_on_worker_loss is True
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
class TestRecoveryConfig:
|
|
195
|
-
"""Tests for recovery configuration options."""
|
|
196
|
-
|
|
197
|
-
def test_config_defaults(self):
|
|
198
|
-
"""Config should have correct default values."""
|
|
199
|
-
config = PyWorkflowConfig()
|
|
200
|
-
|
|
201
|
-
assert (
|
|
202
|
-
config.default_recover_on_worker_loss is None
|
|
203
|
-
) # None = True for durable, False for transient
|
|
204
|
-
assert config.default_max_recovery_attempts == 3
|
|
205
|
-
|
|
206
|
-
def test_config_custom_values(self):
|
|
207
|
-
"""Config should accept custom values."""
|
|
208
|
-
config = PyWorkflowConfig(
|
|
209
|
-
default_recover_on_worker_loss=False,
|
|
210
|
-
default_max_recovery_attempts=5,
|
|
211
|
-
)
|
|
212
|
-
|
|
213
|
-
assert config.default_recover_on_worker_loss is False
|
|
214
|
-
assert config.default_max_recovery_attempts == 5
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
class TestReplayWorkflowInterrupted:
|
|
218
|
-
"""Tests for replay mechanism handling WORKFLOW_INTERRUPTED."""
|
|
219
|
-
|
|
220
|
-
@pytest.mark.asyncio
|
|
221
|
-
async def test_replay_workflow_interrupted_event(self):
|
|
222
|
-
"""Replayer should handle WORKFLOW_INTERRUPTED event without error."""
|
|
223
|
-
from pyworkflow.context import LocalContext
|
|
224
|
-
from pyworkflow.engine.replay import EventReplayer
|
|
225
|
-
|
|
226
|
-
ctx = LocalContext(
|
|
227
|
-
run_id="test_run",
|
|
228
|
-
workflow_name="test_workflow",
|
|
229
|
-
storage=None,
|
|
230
|
-
event_log=[],
|
|
231
|
-
durable=False,
|
|
232
|
-
)
|
|
233
|
-
|
|
234
|
-
event = Event(
|
|
235
|
-
run_id="test_run",
|
|
236
|
-
type=EventType.WORKFLOW_INTERRUPTED,
|
|
237
|
-
data={
|
|
238
|
-
"reason": "worker_lost",
|
|
239
|
-
"recovery_attempt": 1,
|
|
240
|
-
"last_event_sequence": 3,
|
|
241
|
-
},
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
replayer = EventReplayer()
|
|
245
|
-
await replayer._apply_event(ctx, event)
|
|
246
|
-
|
|
247
|
-
# WORKFLOW_INTERRUPTED is informational, doesn't change state
|
|
248
|
-
# Just verify it doesn't raise an exception
|
|
249
|
-
|
|
250
|
-
@pytest.mark.asyncio
|
|
251
|
-
async def test_replay_with_interrupted_event_in_sequence(self):
|
|
252
|
-
"""Replayer should handle WORKFLOW_INTERRUPTED in a sequence of events."""
|
|
253
|
-
from pyworkflow.context import LocalContext
|
|
254
|
-
from pyworkflow.engine.replay import EventReplayer
|
|
255
|
-
from pyworkflow.serialization.encoder import serialize
|
|
256
|
-
|
|
257
|
-
ctx = LocalContext(
|
|
258
|
-
run_id="test_run",
|
|
259
|
-
workflow_name="test_workflow",
|
|
260
|
-
storage=None,
|
|
261
|
-
event_log=[],
|
|
262
|
-
durable=True,
|
|
263
|
-
)
|
|
264
|
-
|
|
265
|
-
events = [
|
|
266
|
-
Event(
|
|
267
|
-
run_id="test_run",
|
|
268
|
-
type=EventType.WORKFLOW_STARTED,
|
|
269
|
-
data={"workflow_name": "test_workflow", "args": "[]", "kwargs": "{}"},
|
|
270
|
-
sequence=1,
|
|
271
|
-
),
|
|
272
|
-
Event(
|
|
273
|
-
run_id="test_run",
|
|
274
|
-
type=EventType.STEP_COMPLETED,
|
|
275
|
-
data={"step_id": "step_1", "result": serialize(42)},
|
|
276
|
-
sequence=2,
|
|
277
|
-
),
|
|
278
|
-
Event(
|
|
279
|
-
run_id="test_run",
|
|
280
|
-
type=EventType.WORKFLOW_INTERRUPTED,
|
|
281
|
-
data={"reason": "worker_lost", "recovery_attempt": 1},
|
|
282
|
-
sequence=3,
|
|
283
|
-
),
|
|
284
|
-
]
|
|
285
|
-
|
|
286
|
-
replayer = EventReplayer()
|
|
287
|
-
await replayer.replay(ctx, events)
|
|
288
|
-
|
|
289
|
-
# Step result should be cached
|
|
290
|
-
assert ctx.get_step_result("step_1") == 42
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
class TestStorageUpdateRecoveryAttempts:
|
|
294
|
-
"""Tests for storage backend update_run_recovery_attempts method."""
|
|
295
|
-
|
|
296
|
-
@pytest.mark.asyncio
|
|
297
|
-
async def test_memory_storage_update_recovery_attempts(self):
|
|
298
|
-
"""InMemoryStorageBackend should update recovery_attempts."""
|
|
299
|
-
from pyworkflow.storage.memory import InMemoryStorageBackend
|
|
300
|
-
|
|
301
|
-
storage = InMemoryStorageBackend()
|
|
302
|
-
|
|
303
|
-
run = WorkflowRun(
|
|
304
|
-
run_id="test_run",
|
|
305
|
-
workflow_name="test_workflow",
|
|
306
|
-
status=RunStatus.RUNNING,
|
|
307
|
-
recovery_attempts=0,
|
|
308
|
-
)
|
|
309
|
-
await storage.create_run(run)
|
|
310
|
-
|
|
311
|
-
await storage.update_run_recovery_attempts("test_run", 2)
|
|
312
|
-
|
|
313
|
-
updated_run = await storage.get_run("test_run")
|
|
314
|
-
assert updated_run.recovery_attempts == 2
|
|
315
|
-
|
|
316
|
-
@pytest.mark.asyncio
|
|
317
|
-
async def test_file_storage_update_recovery_attempts(self, tmp_path):
|
|
318
|
-
"""FileStorageBackend should update recovery_attempts."""
|
|
319
|
-
from pyworkflow.storage.file import FileStorageBackend
|
|
320
|
-
|
|
321
|
-
storage = FileStorageBackend(base_path=str(tmp_path))
|
|
322
|
-
|
|
323
|
-
run = WorkflowRun(
|
|
324
|
-
run_id="test_run",
|
|
325
|
-
workflow_name="test_workflow",
|
|
326
|
-
status=RunStatus.RUNNING,
|
|
327
|
-
recovery_attempts=0,
|
|
328
|
-
)
|
|
329
|
-
await storage.create_run(run)
|
|
330
|
-
|
|
331
|
-
await storage.update_run_recovery_attempts("test_run", 3)
|
|
332
|
-
|
|
333
|
-
updated_run = await storage.get_run("test_run")
|
|
334
|
-
assert updated_run.recovery_attempts == 3
|