kailash 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +31 -0
- kailash/__main__.py +11 -0
- kailash/cli/__init__.py +5 -0
- kailash/cli/commands.py +563 -0
- kailash/manifest.py +778 -0
- kailash/nodes/__init__.py +23 -0
- kailash/nodes/ai/__init__.py +26 -0
- kailash/nodes/ai/agents.py +417 -0
- kailash/nodes/ai/models.py +488 -0
- kailash/nodes/api/__init__.py +52 -0
- kailash/nodes/api/auth.py +567 -0
- kailash/nodes/api/graphql.py +480 -0
- kailash/nodes/api/http.py +598 -0
- kailash/nodes/api/rate_limiting.py +572 -0
- kailash/nodes/api/rest.py +665 -0
- kailash/nodes/base.py +1032 -0
- kailash/nodes/base_async.py +128 -0
- kailash/nodes/code/__init__.py +32 -0
- kailash/nodes/code/python.py +1021 -0
- kailash/nodes/data/__init__.py +125 -0
- kailash/nodes/data/readers.py +496 -0
- kailash/nodes/data/sharepoint_graph.py +623 -0
- kailash/nodes/data/sql.py +380 -0
- kailash/nodes/data/streaming.py +1168 -0
- kailash/nodes/data/vector_db.py +964 -0
- kailash/nodes/data/writers.py +529 -0
- kailash/nodes/logic/__init__.py +6 -0
- kailash/nodes/logic/async_operations.py +702 -0
- kailash/nodes/logic/operations.py +551 -0
- kailash/nodes/transform/__init__.py +5 -0
- kailash/nodes/transform/processors.py +379 -0
- kailash/runtime/__init__.py +6 -0
- kailash/runtime/async_local.py +356 -0
- kailash/runtime/docker.py +697 -0
- kailash/runtime/local.py +434 -0
- kailash/runtime/parallel.py +557 -0
- kailash/runtime/runner.py +110 -0
- kailash/runtime/testing.py +347 -0
- kailash/sdk_exceptions.py +307 -0
- kailash/tracking/__init__.py +7 -0
- kailash/tracking/manager.py +885 -0
- kailash/tracking/metrics_collector.py +342 -0
- kailash/tracking/models.py +535 -0
- kailash/tracking/storage/__init__.py +0 -0
- kailash/tracking/storage/base.py +113 -0
- kailash/tracking/storage/database.py +619 -0
- kailash/tracking/storage/filesystem.py +543 -0
- kailash/utils/__init__.py +0 -0
- kailash/utils/export.py +924 -0
- kailash/utils/templates.py +680 -0
- kailash/visualization/__init__.py +62 -0
- kailash/visualization/api.py +732 -0
- kailash/visualization/dashboard.py +951 -0
- kailash/visualization/performance.py +808 -0
- kailash/visualization/reports.py +1471 -0
- kailash/workflow/__init__.py +15 -0
- kailash/workflow/builder.py +245 -0
- kailash/workflow/graph.py +827 -0
- kailash/workflow/mermaid_visualizer.py +628 -0
- kailash/workflow/mock_registry.py +63 -0
- kailash/workflow/runner.py +302 -0
- kailash/workflow/state.py +238 -0
- kailash/workflow/visualization.py +588 -0
- kailash-0.1.0.dist-info/METADATA +710 -0
- kailash-0.1.0.dist-info/RECORD +69 -0
- kailash-0.1.0.dist-info/WHEEL +5 -0
- kailash-0.1.0.dist-info/entry_points.txt +2 -0
- kailash-0.1.0.dist-info/licenses/LICENSE +21 -0
- kailash-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,885 @@
|
|
1
|
+
"""Task manager for workflow execution tracking."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
from datetime import datetime, timedelta
|
5
|
+
from typing import Any, Dict, List, Optional
|
6
|
+
|
7
|
+
from kailash.sdk_exceptions import StorageException, TaskException, TaskStateError
|
8
|
+
|
9
|
+
from .models import (
|
10
|
+
RunSummary,
|
11
|
+
TaskMetrics,
|
12
|
+
TaskRun,
|
13
|
+
TaskStatus,
|
14
|
+
TaskSummary,
|
15
|
+
WorkflowRun,
|
16
|
+
)
|
17
|
+
from .storage.base import StorageBackend
|
18
|
+
from .storage.filesystem import FileSystemStorage
|
19
|
+
|
20
|
+
logger = logging.getLogger(__name__)
|
21
|
+
|
22
|
+
|
23
|
+
class TaskManager:
|
24
|
+
"""Manages task tracking for workflow executions."""
|
25
|
+
|
26
|
+
def __init__(self, storage_backend: Optional[StorageBackend] = None):
|
27
|
+
"""Initialize task manager.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
storage_backend: Storage backend for persistence. Defaults to FileSystemStorage.
|
31
|
+
|
32
|
+
Raises:
|
33
|
+
TaskException: If initialization fails
|
34
|
+
"""
|
35
|
+
try:
|
36
|
+
self.storage = storage_backend or FileSystemStorage()
|
37
|
+
self.logger = logger
|
38
|
+
|
39
|
+
# In-memory caches
|
40
|
+
self._runs: Dict[str, WorkflowRun] = {}
|
41
|
+
self._tasks: Dict[str, TaskRun] = {}
|
42
|
+
except Exception as e:
|
43
|
+
raise TaskException(f"Failed to initialize task manager: {e}") from e
|
44
|
+
|
45
|
+
def create_run(
|
46
|
+
self, workflow_name: str, metadata: Optional[Dict[str, Any]] = None
|
47
|
+
) -> str:
|
48
|
+
"""Create a new workflow run.
|
49
|
+
|
50
|
+
Args:
|
51
|
+
workflow_name: Name of the workflow
|
52
|
+
metadata: Optional metadata for the run
|
53
|
+
|
54
|
+
Returns:
|
55
|
+
Run ID
|
56
|
+
|
57
|
+
Raises:
|
58
|
+
TaskException: If run creation fails
|
59
|
+
StorageException: If storage operation fails
|
60
|
+
"""
|
61
|
+
if not workflow_name:
|
62
|
+
raise TaskException("Workflow name is required")
|
63
|
+
|
64
|
+
try:
|
65
|
+
run = WorkflowRun(workflow_name=workflow_name, metadata=metadata or {})
|
66
|
+
except Exception as e:
|
67
|
+
raise TaskException(f"Failed to create workflow run: {e}") from e
|
68
|
+
|
69
|
+
# Store in memory and persist
|
70
|
+
self._runs[run.run_id] = run
|
71
|
+
|
72
|
+
try:
|
73
|
+
self.storage.save_run(run)
|
74
|
+
except Exception as e:
|
75
|
+
# Remove from cache if storage fails
|
76
|
+
self._runs.pop(run.run_id, None)
|
77
|
+
raise StorageException(f"Failed to persist workflow run: {e}") from e
|
78
|
+
|
79
|
+
self.logger.info(f"Created workflow run: {run.run_id}")
|
80
|
+
return run.run_id
|
81
|
+
|
82
|
+
def update_run_status(
|
83
|
+
self, run_id: str, status: str, error: Optional[str] = None
|
84
|
+
) -> None:
|
85
|
+
"""Update workflow run status.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
run_id: Run ID
|
89
|
+
status: New status
|
90
|
+
error: Optional error message
|
91
|
+
|
92
|
+
Raises:
|
93
|
+
TaskException: If run not found
|
94
|
+
StorageException: If storage operation fails
|
95
|
+
TaskStateError: If status transition is invalid
|
96
|
+
"""
|
97
|
+
if not run_id:
|
98
|
+
raise TaskException("Run ID is required")
|
99
|
+
|
100
|
+
run = self._runs.get(run_id)
|
101
|
+
if not run:
|
102
|
+
try:
|
103
|
+
run = self.storage.load_run(run_id)
|
104
|
+
except Exception as e:
|
105
|
+
raise StorageException(f"Failed to load run '{run_id}': {e}") from e
|
106
|
+
|
107
|
+
if not run:
|
108
|
+
raise TaskException(
|
109
|
+
f"Run '{run_id}' not found. Available runs: {list(self._runs.keys())}"
|
110
|
+
)
|
111
|
+
self._runs[run_id] = run
|
112
|
+
|
113
|
+
try:
|
114
|
+
run.update_status(status, error)
|
115
|
+
except ValueError as e:
|
116
|
+
raise TaskStateError(
|
117
|
+
f"Invalid status transition for run '{run_id}': {e}"
|
118
|
+
) from e
|
119
|
+
except Exception as e:
|
120
|
+
raise TaskException(f"Failed to update run status: {e}") from e
|
121
|
+
|
122
|
+
try:
|
123
|
+
self.storage.save_run(run)
|
124
|
+
except Exception as e:
|
125
|
+
raise StorageException(f"Failed to persist run status update: {e}") from e
|
126
|
+
|
127
|
+
self.logger.info(f"Updated run {run_id} status to: {status}")
|
128
|
+
|
129
|
+
def create_task(
|
130
|
+
self,
|
131
|
+
node_id: str,
|
132
|
+
input_data: Optional[Dict[str, Any]] = None,
|
133
|
+
metadata: Optional[Dict[str, Any]] = None,
|
134
|
+
run_id: str = "test-run-id",
|
135
|
+
node_type: str = "default-node-type",
|
136
|
+
dependencies: Optional[List[str]] = None,
|
137
|
+
started_at: Optional[datetime] = None,
|
138
|
+
) -> TaskRun:
|
139
|
+
"""Create a new task.
|
140
|
+
|
141
|
+
Args:
|
142
|
+
node_id: Node ID in the workflow
|
143
|
+
input_data: Input data for the task
|
144
|
+
metadata: Additional metadata
|
145
|
+
run_id: Associated run ID (defaults to "test-run-id" for backward compatibility)
|
146
|
+
node_type: Type of node (defaults to "default-node-type" for backward compatibility)
|
147
|
+
dependencies: List of task IDs this task depends on
|
148
|
+
started_at: When the task started
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
TaskRun instance
|
152
|
+
|
153
|
+
Raises:
|
154
|
+
TaskException: If task creation fails
|
155
|
+
StorageException: If storage operation fails
|
156
|
+
"""
|
157
|
+
if not node_id:
|
158
|
+
raise TaskException("Node ID is required")
|
159
|
+
|
160
|
+
try:
|
161
|
+
task = TaskRun(
|
162
|
+
run_id=run_id,
|
163
|
+
node_id=node_id,
|
164
|
+
node_type=node_type,
|
165
|
+
started_at=started_at,
|
166
|
+
input_data=input_data,
|
167
|
+
metadata=metadata or {},
|
168
|
+
dependencies=dependencies or [],
|
169
|
+
)
|
170
|
+
except Exception as e:
|
171
|
+
raise TaskException(f"Failed to create task: {e}") from e
|
172
|
+
|
173
|
+
# Store in memory and persist
|
174
|
+
self._tasks[task.task_id] = task
|
175
|
+
|
176
|
+
try:
|
177
|
+
self.storage.save_task(task)
|
178
|
+
except Exception as e:
|
179
|
+
# Remove from cache if storage fails
|
180
|
+
self._tasks.pop(task.task_id, None)
|
181
|
+
raise StorageException(f"Failed to persist task: {e}") from e
|
182
|
+
|
183
|
+
# Add task to run
|
184
|
+
run = self._runs.get(run_id)
|
185
|
+
if run:
|
186
|
+
try:
|
187
|
+
run.add_task(task.task_id)
|
188
|
+
self.storage.save_run(run)
|
189
|
+
except Exception as e:
|
190
|
+
self.logger.warning(f"Failed to add task to run: {e}")
|
191
|
+
# Continue - task is created, just not linked to run
|
192
|
+
|
193
|
+
self.logger.info(f"Created task: {task.task_id} for node {node_id}")
|
194
|
+
return task
|
195
|
+
|
196
|
+
def update_task_status(
|
197
|
+
self,
|
198
|
+
task_id: str,
|
199
|
+
status: TaskStatus,
|
200
|
+
result: Optional[Dict[str, Any]] = None,
|
201
|
+
error: Optional[str] = None,
|
202
|
+
ended_at: Optional[datetime] = None,
|
203
|
+
metadata: Optional[Dict[str, Any]] = None,
|
204
|
+
) -> None:
|
205
|
+
"""Update task status.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
task_id: Task ID
|
209
|
+
status: New status
|
210
|
+
result: Task result
|
211
|
+
error: Error message
|
212
|
+
ended_at: When the task ended
|
213
|
+
metadata: Additional metadata
|
214
|
+
|
215
|
+
Raises:
|
216
|
+
TaskException: If task not found
|
217
|
+
StorageException: If storage operation fails
|
218
|
+
TaskStateError: If status transition is invalid
|
219
|
+
"""
|
220
|
+
if not task_id:
|
221
|
+
raise TaskException("Task ID is required")
|
222
|
+
|
223
|
+
task = self._tasks.get(task_id)
|
224
|
+
if not task:
|
225
|
+
try:
|
226
|
+
task = self.storage.load_task(task_id)
|
227
|
+
except Exception as e:
|
228
|
+
raise StorageException(f"Failed to load task '{task_id}': {e}") from e
|
229
|
+
|
230
|
+
if not task:
|
231
|
+
raise TaskException(
|
232
|
+
f"Task '{task_id}' not found. Available tasks: {list(self._tasks.keys())}"
|
233
|
+
)
|
234
|
+
self._tasks[task_id] = task
|
235
|
+
|
236
|
+
try:
|
237
|
+
task.update_status(status, result, error, ended_at, metadata)
|
238
|
+
except ValueError as e:
|
239
|
+
raise TaskStateError(
|
240
|
+
f"Invalid status transition for task '{task_id}': {e}"
|
241
|
+
) from e
|
242
|
+
except Exception as e:
|
243
|
+
raise TaskException(f"Failed to update task status: {e}") from e
|
244
|
+
|
245
|
+
try:
|
246
|
+
self.storage.save_task(task)
|
247
|
+
except Exception as e:
|
248
|
+
raise StorageException(f"Failed to persist task status update: {e}") from e
|
249
|
+
|
250
|
+
self.logger.info(f"Updated task {task_id} status to: {status}")
|
251
|
+
|
252
|
+
def get_run(self, run_id: str) -> Optional[WorkflowRun]:
|
253
|
+
"""Get workflow run by ID.
|
254
|
+
|
255
|
+
Args:
|
256
|
+
run_id: Run ID
|
257
|
+
|
258
|
+
Returns:
|
259
|
+
WorkflowRun instance or None
|
260
|
+
|
261
|
+
Raises:
|
262
|
+
StorageException: If storage operation fails
|
263
|
+
"""
|
264
|
+
if not run_id:
|
265
|
+
return None
|
266
|
+
|
267
|
+
run = self._runs.get(run_id)
|
268
|
+
if not run:
|
269
|
+
try:
|
270
|
+
run = self.storage.load_run(run_id)
|
271
|
+
except Exception as e:
|
272
|
+
self.logger.error(f"Failed to load run '{run_id}': {e}")
|
273
|
+
raise StorageException(f"Failed to load run '{run_id}': {e}") from e
|
274
|
+
|
275
|
+
if run:
|
276
|
+
self._runs[run_id] = run
|
277
|
+
return run
|
278
|
+
|
279
|
+
def get_task(self, task_id: str) -> Optional[TaskRun]:
|
280
|
+
"""Get task by ID.
|
281
|
+
|
282
|
+
Args:
|
283
|
+
task_id: Task ID
|
284
|
+
|
285
|
+
Returns:
|
286
|
+
TaskRun instance or None
|
287
|
+
|
288
|
+
Raises:
|
289
|
+
StorageException: If storage operation fails
|
290
|
+
"""
|
291
|
+
if not task_id:
|
292
|
+
return None
|
293
|
+
|
294
|
+
task = self._tasks.get(task_id)
|
295
|
+
if not task:
|
296
|
+
try:
|
297
|
+
task = self.storage.load_task(task_id)
|
298
|
+
except Exception as e:
|
299
|
+
self.logger.error(f"Failed to load task '{task_id}': {e}")
|
300
|
+
raise StorageException(f"Failed to load task '{task_id}': {e}") from e
|
301
|
+
|
302
|
+
if task:
|
303
|
+
self._tasks[task_id] = task
|
304
|
+
return task
|
305
|
+
|
306
|
+
def list_runs(
|
307
|
+
self, workflow_name: Optional[str] = None, status: Optional[str] = None
|
308
|
+
) -> List[RunSummary]:
|
309
|
+
"""List workflow runs.
|
310
|
+
|
311
|
+
Args:
|
312
|
+
workflow_name: Filter by workflow name
|
313
|
+
status: Filter by status
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
List of run summaries
|
317
|
+
|
318
|
+
Raises:
|
319
|
+
StorageException: If storage operation fails
|
320
|
+
"""
|
321
|
+
try:
|
322
|
+
runs = self.storage.list_runs(workflow_name, status)
|
323
|
+
except Exception as e:
|
324
|
+
raise StorageException(f"Failed to list runs: {e}") from e
|
325
|
+
|
326
|
+
summaries = []
|
327
|
+
|
328
|
+
for run in runs:
|
329
|
+
try:
|
330
|
+
tasks = self.list_tasks(run.run_id)
|
331
|
+
task_runs = []
|
332
|
+
|
333
|
+
for task in tasks:
|
334
|
+
try:
|
335
|
+
task_run = self.get_task(task.task_id)
|
336
|
+
if task_run:
|
337
|
+
task_runs.append(task_run)
|
338
|
+
except Exception as e:
|
339
|
+
self.logger.warning(
|
340
|
+
f"Failed to load task '{task.task_id}': {e}"
|
341
|
+
)
|
342
|
+
|
343
|
+
summary = RunSummary.from_workflow_run(run, task_runs)
|
344
|
+
summaries.append(summary)
|
345
|
+
|
346
|
+
except Exception as e:
|
347
|
+
self.logger.warning(
|
348
|
+
f"Failed to create summary for run '{run.run_id}': {e}"
|
349
|
+
)
|
350
|
+
|
351
|
+
return summaries
|
352
|
+
|
353
|
+
def list_tasks(
|
354
|
+
self,
|
355
|
+
run_id: str,
|
356
|
+
node_id: Optional[str] = None,
|
357
|
+
status: Optional[TaskStatus] = None,
|
358
|
+
) -> List[TaskSummary]:
|
359
|
+
"""List tasks for a run.
|
360
|
+
|
361
|
+
Args:
|
362
|
+
run_id: Run ID
|
363
|
+
node_id: Filter by node ID
|
364
|
+
status: Filter by status
|
365
|
+
|
366
|
+
Returns:
|
367
|
+
List of task summaries
|
368
|
+
|
369
|
+
Raises:
|
370
|
+
TaskException: If run_id is not provided
|
371
|
+
StorageException: If storage operation fails
|
372
|
+
"""
|
373
|
+
if not run_id:
|
374
|
+
raise TaskException("Run ID is required")
|
375
|
+
|
376
|
+
try:
|
377
|
+
tasks = self.storage.list_tasks(run_id, node_id, status)
|
378
|
+
except Exception as e:
|
379
|
+
raise StorageException(
|
380
|
+
f"Failed to list tasks for run '{run_id}': {e}"
|
381
|
+
) from e
|
382
|
+
|
383
|
+
summaries = []
|
384
|
+
for task in tasks:
|
385
|
+
try:
|
386
|
+
summary = TaskSummary.from_task_run(task)
|
387
|
+
summaries.append(summary)
|
388
|
+
except Exception as e:
|
389
|
+
self.logger.warning(
|
390
|
+
f"Failed to create summary for task '{task.task_id}': {e}"
|
391
|
+
)
|
392
|
+
|
393
|
+
return summaries
|
394
|
+
|
395
|
+
def get_run_summary(self, run_id: str) -> Optional[RunSummary]:
|
396
|
+
"""Get summary for a specific run.
|
397
|
+
|
398
|
+
Args:
|
399
|
+
run_id: Run ID
|
400
|
+
|
401
|
+
Returns:
|
402
|
+
RunSummary or None
|
403
|
+
|
404
|
+
Raises:
|
405
|
+
StorageException: If storage operation fails
|
406
|
+
"""
|
407
|
+
if not run_id:
|
408
|
+
return None
|
409
|
+
|
410
|
+
try:
|
411
|
+
run = self.get_run(run_id)
|
412
|
+
except Exception as e:
|
413
|
+
self.logger.error(f"Failed to get run '{run_id}': {e}")
|
414
|
+
return None
|
415
|
+
|
416
|
+
if not run:
|
417
|
+
return None
|
418
|
+
|
419
|
+
try:
|
420
|
+
tasks = self.list_tasks(run_id)
|
421
|
+
task_runs = []
|
422
|
+
|
423
|
+
for task in tasks:
|
424
|
+
try:
|
425
|
+
task_run = self.get_task(task.task_id)
|
426
|
+
if task_run:
|
427
|
+
task_runs.append(task_run)
|
428
|
+
except Exception as e:
|
429
|
+
self.logger.warning(f"Failed to load task '{task.task_id}': {e}")
|
430
|
+
|
431
|
+
return RunSummary.from_workflow_run(run, task_runs)
|
432
|
+
|
433
|
+
except Exception as e:
|
434
|
+
self.logger.error(f"Failed to create run summary for '{run_id}': {e}")
|
435
|
+
return None
|
436
|
+
|
437
|
+
def clear_cache(self) -> None:
|
438
|
+
"""Clear in-memory caches."""
|
439
|
+
self._runs.clear()
|
440
|
+
self._tasks.clear()
|
441
|
+
self.logger.info("Cleared task manager cache")
|
442
|
+
|
443
|
+
def complete_task(
|
444
|
+
self, task_id: str, output_data: Optional[Dict[str, Any]] = None
|
445
|
+
) -> None:
|
446
|
+
"""Complete a task successfully.
|
447
|
+
|
448
|
+
Args:
|
449
|
+
task_id: Task ID
|
450
|
+
output_data: Output data for the task
|
451
|
+
|
452
|
+
Raises:
|
453
|
+
TaskException: If task not found
|
454
|
+
StorageException: If storage operation fails
|
455
|
+
"""
|
456
|
+
task = self.get_task(task_id)
|
457
|
+
if not task:
|
458
|
+
raise TaskException(f"Task '{task_id}' not found")
|
459
|
+
|
460
|
+
task.complete(output_data)
|
461
|
+
|
462
|
+
# Add simple metrics if not present
|
463
|
+
if not task.metrics:
|
464
|
+
task.metrics = TaskMetrics(duration=task.duration or 0)
|
465
|
+
|
466
|
+
try:
|
467
|
+
self.storage.save_task(task)
|
468
|
+
except Exception as e:
|
469
|
+
raise StorageException(f"Failed to save completed task: {e}") from e
|
470
|
+
|
471
|
+
self.logger.info(f"Completed task {task_id}")
|
472
|
+
|
473
|
+
def fail_task(self, task_id: str, error_message: str) -> None:
|
474
|
+
"""Mark a task as failed.
|
475
|
+
|
476
|
+
Args:
|
477
|
+
task_id: Task ID
|
478
|
+
error_message: Error message
|
479
|
+
|
480
|
+
Raises:
|
481
|
+
TaskException: If task not found
|
482
|
+
StorageException: If storage operation fails
|
483
|
+
"""
|
484
|
+
task = self.get_task(task_id)
|
485
|
+
if not task:
|
486
|
+
raise TaskException(f"Task '{task_id}' not found")
|
487
|
+
|
488
|
+
task.fail(error_message)
|
489
|
+
|
490
|
+
try:
|
491
|
+
self.storage.save_task(task)
|
492
|
+
except Exception as e:
|
493
|
+
raise StorageException(f"Failed to save failed task: {e}") from e
|
494
|
+
|
495
|
+
self.logger.info(f"Failed task {task_id}: {error_message}")
|
496
|
+
|
497
|
+
def cancel_task(self, task_id: str, reason: str) -> None:
|
498
|
+
"""Cancel a task.
|
499
|
+
|
500
|
+
Args:
|
501
|
+
task_id: Task ID
|
502
|
+
reason: Cancellation reason
|
503
|
+
|
504
|
+
Raises:
|
505
|
+
TaskException: If task not found
|
506
|
+
StorageException: If storage operation fails
|
507
|
+
"""
|
508
|
+
task = self.get_task(task_id)
|
509
|
+
if not task:
|
510
|
+
raise TaskException(f"Task '{task_id}' not found")
|
511
|
+
|
512
|
+
task.cancel(reason)
|
513
|
+
|
514
|
+
try:
|
515
|
+
self.storage.save_task(task)
|
516
|
+
except Exception as e:
|
517
|
+
raise StorageException(f"Failed to save cancelled task: {e}") from e
|
518
|
+
|
519
|
+
self.logger.info(f"Cancelled task {task_id}: {reason}")
|
520
|
+
|
521
|
+
def retry_task(self, task_id: str) -> TaskRun:
|
522
|
+
"""Create a new task as a retry of an existing task.
|
523
|
+
|
524
|
+
Args:
|
525
|
+
task_id: Original task ID
|
526
|
+
|
527
|
+
Returns:
|
528
|
+
New task instance
|
529
|
+
|
530
|
+
Raises:
|
531
|
+
TaskException: If task not found
|
532
|
+
StorageException: If storage operation fails
|
533
|
+
"""
|
534
|
+
original_task = self.get_task(task_id)
|
535
|
+
if not original_task:
|
536
|
+
raise TaskException(f"Task '{task_id}' not found")
|
537
|
+
|
538
|
+
retry_task = original_task.create_retry()
|
539
|
+
|
540
|
+
try:
|
541
|
+
self.storage.save_task(retry_task)
|
542
|
+
except Exception as e:
|
543
|
+
raise StorageException(f"Failed to save retry task: {e}") from e
|
544
|
+
|
545
|
+
self._tasks[retry_task.task_id] = retry_task
|
546
|
+
self.logger.info(f"Created retry task {retry_task.task_id} for {task_id}")
|
547
|
+
|
548
|
+
return retry_task
|
549
|
+
|
550
|
+
def delete_task(self, task_id: str) -> None:
|
551
|
+
"""Delete a task.
|
552
|
+
|
553
|
+
Args:
|
554
|
+
task_id: Task ID
|
555
|
+
|
556
|
+
Raises:
|
557
|
+
TaskException: If task not found
|
558
|
+
StorageException: If storage operation fails
|
559
|
+
"""
|
560
|
+
if task_id in self._tasks:
|
561
|
+
del self._tasks[task_id]
|
562
|
+
|
563
|
+
try:
|
564
|
+
self.storage.delete_task(task_id)
|
565
|
+
except Exception as e:
|
566
|
+
raise StorageException(f"Failed to delete task: {e}") from e
|
567
|
+
|
568
|
+
self.logger.info(f"Deleted task {task_id}")
|
569
|
+
|
570
|
+
def get_tasks_by_status(self, status: TaskStatus) -> List[TaskRun]:
|
571
|
+
"""Get tasks by status.
|
572
|
+
|
573
|
+
Args:
|
574
|
+
status: Status to filter by
|
575
|
+
|
576
|
+
Returns:
|
577
|
+
List of matching tasks
|
578
|
+
|
579
|
+
Raises:
|
580
|
+
StorageException: If storage operation fails
|
581
|
+
"""
|
582
|
+
try:
|
583
|
+
if hasattr(self.storage, "query_tasks"):
|
584
|
+
return self.storage.query_tasks(status=status)
|
585
|
+
else:
|
586
|
+
# Fallback for MockStorage
|
587
|
+
return [t for t in self.storage.get_all_tasks() if t.status == status]
|
588
|
+
except Exception as e:
|
589
|
+
raise StorageException(f"Failed to query tasks by status: {e}") from e
|
590
|
+
|
591
|
+
def get_tasks_by_node(self, node_id: str) -> List[TaskRun]:
|
592
|
+
"""Get tasks by node ID.
|
593
|
+
|
594
|
+
Args:
|
595
|
+
node_id: Node ID to filter by
|
596
|
+
|
597
|
+
Returns:
|
598
|
+
List of matching tasks
|
599
|
+
|
600
|
+
Raises:
|
601
|
+
StorageException: If storage operation fails
|
602
|
+
"""
|
603
|
+
try:
|
604
|
+
if hasattr(self.storage, "query_tasks"):
|
605
|
+
return self.storage.query_tasks(node_id=node_id)
|
606
|
+
else:
|
607
|
+
# Fallback for MockStorage
|
608
|
+
return [t for t in self.storage.get_all_tasks() if t.node_id == node_id]
|
609
|
+
except Exception as e:
|
610
|
+
raise StorageException(f"Failed to query tasks by node: {e}") from e
|
611
|
+
|
612
|
+
def get_task_history(self, task_id: str) -> List[TaskRun]:
|
613
|
+
"""Get task history (original task and all retries).
|
614
|
+
|
615
|
+
Args:
|
616
|
+
task_id: Task ID
|
617
|
+
|
618
|
+
Returns:
|
619
|
+
List of tasks in order (original first, latest retry last)
|
620
|
+
|
621
|
+
Raises:
|
622
|
+
TaskException: If task not found
|
623
|
+
StorageException: If storage operation fails
|
624
|
+
"""
|
625
|
+
task = self.get_task(task_id)
|
626
|
+
if not task:
|
627
|
+
raise TaskException(f"Task '{task_id}' not found")
|
628
|
+
|
629
|
+
# Build history by following parent_task_id chain
|
630
|
+
history = []
|
631
|
+
current = task
|
632
|
+
|
633
|
+
# First, find the original task by following parent_task_id backward
|
634
|
+
while current.parent_task_id:
|
635
|
+
parent = self.get_task(current.parent_task_id)
|
636
|
+
if not parent:
|
637
|
+
break
|
638
|
+
current = parent
|
639
|
+
|
640
|
+
# Now current is the original task, build history forward
|
641
|
+
history.append(current)
|
642
|
+
while True:
|
643
|
+
# Find tasks with this task as parent
|
644
|
+
children = []
|
645
|
+
for t in self.storage.get_all_tasks():
|
646
|
+
if t.parent_task_id == current.task_id:
|
647
|
+
children.append(t)
|
648
|
+
|
649
|
+
if not children:
|
650
|
+
break
|
651
|
+
|
652
|
+
# Find the child with the lowest retry count
|
653
|
+
next_task = min(children, key=lambda t: t.retry_count)
|
654
|
+
history.append(next_task)
|
655
|
+
current = next_task
|
656
|
+
|
657
|
+
return history
|
658
|
+
|
659
|
+
def get_tasks_by_timerange(
|
660
|
+
self, start_time: datetime, end_time: datetime
|
661
|
+
) -> List[TaskRun]:
|
662
|
+
"""Get tasks created between start_time and end_time.
|
663
|
+
|
664
|
+
Args:
|
665
|
+
start_time: Start of time range
|
666
|
+
end_time: End of time range
|
667
|
+
|
668
|
+
Returns:
|
669
|
+
List of matching tasks
|
670
|
+
|
671
|
+
Raises:
|
672
|
+
StorageException: If storage operation fails
|
673
|
+
"""
|
674
|
+
try:
|
675
|
+
if hasattr(self.storage, "query_tasks"):
|
676
|
+
return self.storage.query_tasks(
|
677
|
+
started_after=start_time, completed_before=end_time
|
678
|
+
)
|
679
|
+
else:
|
680
|
+
# Fallback for MockStorage
|
681
|
+
return [
|
682
|
+
t
|
683
|
+
for t in self.storage.get_all_tasks()
|
684
|
+
if t.created_at >= start_time and t.created_at <= end_time
|
685
|
+
]
|
686
|
+
except Exception as e:
|
687
|
+
raise StorageException(f"Failed to query tasks by timerange: {e}") from e
|
688
|
+
|
689
|
+
def get_task_statistics(self) -> Dict[str, Any]:
|
690
|
+
"""Get task statistics.
|
691
|
+
|
692
|
+
Returns:
|
693
|
+
Dictionary with statistics:
|
694
|
+
- total_tasks: Total number of tasks
|
695
|
+
- by_status: Count of tasks by status
|
696
|
+
- by_node: Count of tasks by node ID
|
697
|
+
|
698
|
+
Raises:
|
699
|
+
StorageException: If storage operation fails
|
700
|
+
"""
|
701
|
+
try:
|
702
|
+
tasks = self.storage.get_all_tasks()
|
703
|
+
except Exception as e:
|
704
|
+
raise StorageException(f"Failed to get tasks for statistics: {e}") from e
|
705
|
+
|
706
|
+
by_status = {}
|
707
|
+
by_node = {}
|
708
|
+
|
709
|
+
for task in tasks:
|
710
|
+
# Count by status
|
711
|
+
status = task.status
|
712
|
+
by_status[status] = by_status.get(status, 0) + 1
|
713
|
+
|
714
|
+
# Count by node
|
715
|
+
node = task.node_id
|
716
|
+
by_node[node] = by_node.get(node, 0) + 1
|
717
|
+
|
718
|
+
return {"total_tasks": len(tasks), "by_status": by_status, "by_node": by_node}
|
719
|
+
|
720
|
+
def cleanup_old_tasks(self, days: int = 30) -> int:
|
721
|
+
"""Delete tasks older than specified days.
|
722
|
+
|
723
|
+
Args:
|
724
|
+
days: Age in days
|
725
|
+
|
726
|
+
Returns:
|
727
|
+
Number of tasks deleted
|
728
|
+
|
729
|
+
Raises:
|
730
|
+
StorageException: If storage operation fails
|
731
|
+
"""
|
732
|
+
try:
|
733
|
+
tasks = self.storage.get_all_tasks()
|
734
|
+
except Exception as e:
|
735
|
+
raise StorageException(f"Failed to get tasks for cleanup: {e}") from e
|
736
|
+
|
737
|
+
cutoff = datetime.now() - timedelta(days=days)
|
738
|
+
deleted = 0
|
739
|
+
|
740
|
+
for task in tasks:
|
741
|
+
if task.created_at and task.created_at < cutoff:
|
742
|
+
try:
|
743
|
+
self.delete_task(task.task_id)
|
744
|
+
deleted += 1
|
745
|
+
except Exception as e:
|
746
|
+
self.logger.warning(
|
747
|
+
f"Failed to delete old task {task.task_id}: {e}"
|
748
|
+
)
|
749
|
+
|
750
|
+
return deleted
|
751
|
+
|
752
|
+
def update_task_metrics(self, task_id: str, metrics: TaskMetrics) -> None:
|
753
|
+
"""Update task metrics.
|
754
|
+
|
755
|
+
Args:
|
756
|
+
task_id: Task ID
|
757
|
+
metrics: Metrics to update
|
758
|
+
|
759
|
+
Raises:
|
760
|
+
TaskException: If task not found
|
761
|
+
StorageException: If storage operation fails
|
762
|
+
"""
|
763
|
+
task = self.get_task(task_id)
|
764
|
+
if not task:
|
765
|
+
raise TaskException(f"Task '{task_id}' not found")
|
766
|
+
|
767
|
+
task.metrics = metrics
|
768
|
+
|
769
|
+
try:
|
770
|
+
self.storage.save_task(task)
|
771
|
+
except Exception as e:
|
772
|
+
raise StorageException(f"Failed to update task metrics: {e}") from e
|
773
|
+
|
774
|
+
self.logger.info(f"Updated metrics for task {task_id}")
|
775
|
+
|
776
|
+
def get_running_tasks(self) -> List[TaskRun]:
|
777
|
+
"""Get all currently running tasks.
|
778
|
+
|
779
|
+
Returns:
|
780
|
+
List of running tasks
|
781
|
+
|
782
|
+
Raises:
|
783
|
+
StorageException: If storage operation fails
|
784
|
+
"""
|
785
|
+
return self.get_tasks_by_status(TaskStatus.RUNNING)
|
786
|
+
|
787
|
+
def get_task_dependencies(self, task_id: str) -> List[TaskRun]:
|
788
|
+
"""Get tasks that are dependencies for the given task.
|
789
|
+
|
790
|
+
Args:
|
791
|
+
task_id: Task ID
|
792
|
+
|
793
|
+
Returns:
|
794
|
+
List of dependency tasks
|
795
|
+
|
796
|
+
Raises:
|
797
|
+
TaskException: If task not found
|
798
|
+
StorageException: If storage operation fails
|
799
|
+
"""
|
800
|
+
task = self.get_task(task_id)
|
801
|
+
if not task:
|
802
|
+
raise TaskException(f"Task '{task_id}' not found")
|
803
|
+
|
804
|
+
dependencies = []
|
805
|
+
for dep_id in task.dependencies:
|
806
|
+
dep = self.get_task(dep_id)
|
807
|
+
if dep:
|
808
|
+
dependencies.append(dep)
|
809
|
+
|
810
|
+
return dependencies
|
811
|
+
|
812
|
+
def save_task(self, task: TaskRun) -> None:
|
813
|
+
"""Save a task to storage.
|
814
|
+
|
815
|
+
This is a convenience method that directly saves a task instance to storage.
|
816
|
+
For new tasks, prefer using create_task() instead.
|
817
|
+
|
818
|
+
Args:
|
819
|
+
task: TaskRun instance to save
|
820
|
+
|
821
|
+
Raises:
|
822
|
+
StorageException: If storage operation fails
|
823
|
+
"""
|
824
|
+
try:
|
825
|
+
# Store in cache
|
826
|
+
self._tasks[task.task_id] = task
|
827
|
+
|
828
|
+
# Save to storage
|
829
|
+
self.storage.save_task(task)
|
830
|
+
self.logger.info(f"Saved task: {task.task_id}")
|
831
|
+
|
832
|
+
# Add task to run if needed
|
833
|
+
run = self._runs.get(task.run_id)
|
834
|
+
if run and task.task_id not in run.tasks:
|
835
|
+
run.add_task(task.task_id)
|
836
|
+
self.storage.save_run(run)
|
837
|
+
|
838
|
+
except Exception as e:
|
839
|
+
raise StorageException(f"Failed to save task: {e}") from e
|
840
|
+
|
841
|
+
def get_run_tasks(self, run_id: str) -> List[TaskRun]:
|
842
|
+
"""Get all tasks for a specific run.
|
843
|
+
|
844
|
+
Args:
|
845
|
+
run_id: Run ID to get tasks for
|
846
|
+
|
847
|
+
Returns:
|
848
|
+
List of tasks in the run
|
849
|
+
"""
|
850
|
+
run = self.get_run(run_id)
|
851
|
+
if not run:
|
852
|
+
return []
|
853
|
+
|
854
|
+
tasks = []
|
855
|
+
for task_id in run.tasks:
|
856
|
+
task = self.get_task(task_id)
|
857
|
+
if task:
|
858
|
+
tasks.append(task)
|
859
|
+
|
860
|
+
return tasks
|
861
|
+
|
862
|
+
def get_workflow_tasks(self, workflow_id: str) -> List[TaskRun]:
|
863
|
+
"""Get all tasks for a workflow.
|
864
|
+
|
865
|
+
This is a compatibility method that returns all tasks across all runs for a workflow.
|
866
|
+
In practice, tasks are tracked per run, not per workflow.
|
867
|
+
|
868
|
+
Args:
|
869
|
+
workflow_id: Workflow ID (used to filter runs)
|
870
|
+
|
871
|
+
Returns:
|
872
|
+
List of all TaskRun objects for the workflow
|
873
|
+
|
874
|
+
Raises:
|
875
|
+
StorageException: If storage operation fails
|
876
|
+
"""
|
877
|
+
try:
|
878
|
+
# Get all tasks from storage
|
879
|
+
all_tasks = self.storage.get_all_tasks()
|
880
|
+
|
881
|
+
# For now, return all tasks since we don't have a good way to filter by workflow_id
|
882
|
+
# In a real implementation, we'd need to track workflow_id in tasks or runs
|
883
|
+
return all_tasks
|
884
|
+
except Exception as e:
|
885
|
+
raise StorageException(f"Failed to get workflow tasks: {e}") from e
|