205 lines
6.8 KiB
Python
205 lines
6.8 KiB
Python
"""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"
|
||
)
|