Skip to content

Commit 2c24026

Browse files
committed
Attempt to render DAG from DB
fallback to tasks file
1 parent 8e32caf commit 2c24026

2 files changed

Lines changed: 118 additions & 26 deletions

File tree

src/rushti/commands.py

Lines changed: 52 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,37 +1664,63 @@ def _stats_visualize(args) -> None:
16641664

16651665
# --- Attempt DAG generation ---
16661666
dag_generated = False
1667-
taskfile_path = None
16681667

1669-
# Find the first accessible taskfile_path from runs (most recent first)
1668+
# Prefer DB-based DAG (no taskfile on disk needed); fall back to taskfile if unavailable.
16701669
runs = data["runs"]
1671-
for run in runs:
1672-
candidate = run.get("taskfile_path")
1673-
if candidate and not candidate.startswith("TM1:") and os.path.isfile(candidate):
1674-
taskfile_path = candidate
1675-
break
1670+
workflow_runs = [r for r in runs if (r.get("workflow") or "") == args.workflow]
1671+
latest_run = workflow_runs[0] if workflow_runs else None
16761672

1677-
if taskfile_path:
1673+
dag_generated_from_db = False
1674+
if latest_run:
16781675
try:
1679-
from rushti.taskfile_ops import visualize_dag
1680-
1681-
# Ensure output directory exists
1682-
Path(dag_path).parent.mkdir(parents=True, exist_ok=True)
1683-
1684-
visualize_dag(
1685-
source=taskfile_path,
1686-
output_path=dag_path,
1687-
dashboard_url=dashboard_filename,
1688-
)
1689-
dag_generated = True
1690-
print(f"DAG visualization generated: {dag_path}")
1676+
from rushti.taskfile_ops import visualize_dag_from_db_results
1677+
1678+
latest_run_id = latest_run["run_id"]
1679+
latest_task_results = [
1680+
tr for tr in data["task_results"] if tr["run_id"] == latest_run_id
1681+
]
1682+
1683+
if latest_task_results:
1684+
Path(dag_path).parent.mkdir(parents=True, exist_ok=True)
1685+
visualize_dag_from_db_results(
1686+
task_results=latest_task_results,
1687+
output_path=dag_path,
1688+
dashboard_url=dashboard_filename,
1689+
)
1690+
dag_generated = True
1691+
dag_generated_from_db = True
1692+
print(f"DAG visualization generated: {dag_path}")
16911693
except Exception as e:
1692-
logger.warning(f"Could not generate DAG visualization: {e}")
1693-
print(f"Warning: DAG visualization skipped ({e})")
1694-
else:
1695-
print(
1696-
"Warning: No accessible taskfile found in run history, skipping DAG visualization"
1697-
)
1694+
logger.warning(f"Could not generate DAG from DB: {e}")
1695+
1696+
if not dag_generated_from_db:
1697+
# Fall back to taskfile on disk (most recent accessible one for this workflow)
1698+
taskfile_path = None
1699+
for run in workflow_runs:
1700+
candidate = run.get("taskfile_path")
1701+
if candidate and not candidate.startswith("TM1:") and os.path.isfile(candidate):
1702+
taskfile_path = candidate
1703+
break
1704+
1705+
if taskfile_path:
1706+
try:
1707+
from rushti.taskfile_ops import visualize_dag
1708+
1709+
Path(dag_path).parent.mkdir(parents=True, exist_ok=True)
1710+
visualize_dag(
1711+
source=taskfile_path,
1712+
output_path=dag_path,
1713+
dashboard_url=dashboard_filename,
1714+
)
1715+
dag_generated = True
1716+
print(f"DAG visualization generated from taskfile: {dag_path}")
1717+
except Exception as e:
1718+
logger.warning(f"Could not generate DAG visualization: {e}")
1719+
print(f"Warning: DAG visualization skipped ({e})")
1720+
else:
1721+
print(
1722+
"Warning: No DB task results or accessible taskfile found, skipping DAG visualization"
1723+
)
16981724

16991725
# --- Generate dashboard ---
17001726
output_file = generate_dashboard(

src/rushti/taskfile_ops.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,72 @@ def validate_taskfile_full(
797797
return result
798798

799799

800+
def visualize_dag_from_db_results(
801+
task_results: List[Dict[str, Any]],
802+
output_path: str,
803+
dashboard_url: Optional[str] = None,
804+
) -> str:
805+
"""Generate interactive HTML DAG visualization from DB task results.
806+
807+
Builds the DAG from task result records stored in the stats database,
808+
so no taskfile on disk is required.
809+
810+
:param task_results: List of task result dicts for a single run (from get_visualization_data)
811+
:param output_path: Path to output HTML file
812+
:param dashboard_url: Optional relative URL back to the dashboard HTML
813+
:return: Path to the generated HTML file
814+
"""
815+
tasks_by_id: Dict[str, "TaskDefinition"] = {}
816+
adjacency: Dict[str, List[str]] = {}
817+
818+
for tr in task_results:
819+
task_id = tr.get("task_id")
820+
if not task_id or task_id in tasks_by_id:
821+
continue
822+
823+
params = tr.get("parameters")
824+
if isinstance(params, str):
825+
try:
826+
params = json.loads(params)
827+
except Exception:
828+
params = {}
829+
params = params or {}
830+
831+
predecessors = tr.get("predecessors")
832+
if isinstance(predecessors, str):
833+
try:
834+
predecessors = json.loads(predecessors)
835+
except Exception:
836+
predecessors = []
837+
predecessors = predecessors or []
838+
839+
tasks_by_id[task_id] = TaskDefinition(
840+
id=task_id,
841+
instance=tr.get("instance") or "",
842+
process=tr.get("process") or "",
843+
parameters=params,
844+
predecessors=predecessors,
845+
stage=tr.get("stage"),
846+
)
847+
848+
for pred in predecessors:
849+
if pred not in adjacency:
850+
adjacency[pred] = []
851+
adjacency[pred].append(task_id)
852+
853+
output_path_obj = Path(output_path)
854+
output_base = (
855+
str(output_path_obj.with_suffix("")) if output_path_obj.suffix else str(output_path_obj)
856+
)
857+
858+
return _visualize_dag_html(
859+
adjacency=adjacency,
860+
tasks_by_id=tasks_by_id,
861+
filename=output_base,
862+
dashboard_url=dashboard_url,
863+
)
864+
865+
800866
def _check_dag_cycles(tasks: List[TaskDefinition]) -> List[str]:
801867
"""Check for cycles in task dependencies.
802868

0 commit comments

Comments
 (0)