Files
qt6-migration-pipeline/app/migration_pipeline/orchestrator.py
T

205 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Batch orchestrator – coordinates the full migration of one batch.
Flow:
1. Read the prioritised file list (one path per line, in order).
2. Create git worktrees for Track A and Track B in parallel.
3. Run cmake --configure in each worktree.
4. Process files in priority order, with at most MAX_CONCURRENT_TASKS
workers per track. Both tracks run concurrently.
5. Commit a batch-summary to each branch.
6. Produce a comparison report (Track A vs Track B).
"""
from __future__ import annotations
import asyncio
import logging
import time
from pathlib import Path
import httpx
from migration_pipeline.build_runner import cmake_configure
from migration_pipeline.config import (
MAX_CONCURRENT_TASKS,
REPORTS_DIR,
TRACK_A_ID,
TRACK_B_ID,
WORKSPACE_ROOT,
)
from migration_pipeline.git_manager import (
commit_batch_summary,
setup_worktree,
)
from migration_pipeline.migrator import migrate_file
from migration_pipeline.models import BatchResult, FileResult, FileStatus, Track
from migration_pipeline.reporter import write_comparison_report
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# File-list loading
# ---------------------------------------------------------------------------
def load_file_list(list_path: Path, source_root: Path) -> list[str]:
"""Read the external priority file list.
Accepts:
- Plain text: one file path per line (blank lines and #-comments ignored).
- The paths may be absolute or relative to source_root.
"""
base = source_root.resolve()
files: list[str] = []
for raw in list_path.read_text().splitlines():
stripped = raw.strip()
if not stripped or stripped.startswith("#"):
continue
p = Path(stripped)
if not p.is_absolute():
p = base / p
files.append(str(p.resolve()))
return files
# ---------------------------------------------------------------------------
# Single-track worker pool
# ---------------------------------------------------------------------------
async def _run_track(
client: httpx.AsyncClient,
file_list: list[str],
source_root: Path,
track: Track,
worktree_root: Path,
build_dir: Path,
batch_id: str,
) -> BatchResult:
semaphore = asyncio.Semaphore(MAX_CONCURRENT_TASKS)
source_root = source_root.resolve()
async def _worker(fp: str) -> FileResult:
async with semaphore:
abs_file = Path(fp).resolve()
try:
rel_file = abs_file.relative_to(source_root)
except ValueError as exc:
raise ValueError(
f"File '{abs_file}' is not under source-root '{source_root}'"
) from exc
source_file = worktree_root / rel_file
return await migrate_file(
client=client,
source_file=source_file,
worktree_root=worktree_root,
build_dir=build_dir,
track=track,
batch_id=batch_id,
)
logger.info("[TRACK %s] Starting %d files", track.value, len(file_list))
results: list[FileResult] = await asyncio.gather(
*[_worker(fp) for fp in file_list]
)
successes = [r for r in results if r.status == FileStatus.SUCCESS.value]
quarantine = [r for r in results if r.status == FileStatus.QUARANTINED.value]
elapsed = [r.elapsed_seconds for r in results]
warnings = sum(r.gate.total_warnings for r in results)
return BatchResult(
batch_id=batch_id,
track=track.value,
branch=f"migration/{batch_id}/{track.value}",
total_files=len(results),
success_count=len(successes),
quarantine_count=len(quarantine),
total_warnings=warnings,
mean_elapsed_seconds=sum(elapsed) / len(elapsed) if elapsed else 0.0,
results=results,
)
# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------
async def run_batch(
batch_id: str,
file_list_path: Path,
repo_root: Path,
source_root: Path,
) -> tuple[BatchResult, BatchResult]:
"""Run the full batch on both tracks in parallel.
Returns ``(result_track_a, result_track_b)``.
"""
file_list = load_file_list(file_list_path, source_root)
logger.info(
"[BATCH %s] Loaded %d files from %s",
batch_id, len(file_list), file_list_path,
)
workspace = WORKSPACE_ROOT.resolve()
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
# ---- Set up worktrees for both tracks ----------------------------------
worktree_a = setup_worktree(batch_id, TRACK_A_ID, repo_root, workspace)
worktree_b = setup_worktree(batch_id, TRACK_B_ID, repo_root, workspace)
build_dir_a = worktree_a / "build"
build_dir_b = worktree_b / "build"
# ---- Configure cmake in both worktrees (in parallel) ------------------
logger.info("[BATCH %s] Configuring cmake for both tracks...", batch_id)
await asyncio.gather(
cmake_configure(worktree_a, build_dir_a),
cmake_configure(worktree_b, build_dir_b),
)
# ---- Run both tracks concurrently -------------------------------------
async with httpx.AsyncClient() as client:
result_a, result_b = await asyncio.gather(
_run_track(
client, file_list, source_root, Track.A, worktree_a, build_dir_a, batch_id
),
_run_track(
client, file_list, source_root, Track.B, worktree_b, build_dir_b, batch_id
),
)
# ---- Final batch commit on each branch --------------------------------
commit_batch_summary(worktree_a, TRACK_A_ID, batch_id)
commit_batch_summary(worktree_b, TRACK_B_ID, batch_id)
# ---- Write comparison report ------------------------------------------
report_path = REPORTS_DIR / f"{batch_id}_comparison.md"
write_comparison_report(result_a, result_b, report_path)
logger.info("[BATCH %s] Report written to %s", batch_id, report_path)
_print_summary(result_a, result_b)
return result_a, result_b
# ---------------------------------------------------------------------------
# Console summary
# ---------------------------------------------------------------------------
def _print_summary(a: BatchResult, b: BatchResult) -> None:
print("\n" + "=" * 64)
print(f" Batch: {a.batch_id}")
print("=" * 64)
_print_track_row("Track A (konservativ)", a)
_print_track_row("Track B (modern) ", b)
print("=" * 64 + "\n")
def _print_track_row(label: str, r: BatchResult) -> None:
print(
f" {label}: "
f"{r.success_count}/{r.total_files} OK | "
f"{r.quarantine_count} quarantined | "
f"{r.total_warnings} warnings | "
f"avg {r.mean_elapsed_seconds:.0f}s"
)