Source code for gedih3.doctor.runner

# Copyright (C) 2026, University of Maryland. All Rights Reserved.
# Authors: Tiago de Conto, Amelia Grace Holcomb
# For commercial licensing inquiries, contact UM Ventures at umdtechtransfer@umd.edu

"""Diagnosis registry + orchestrator for gh3_doctor.

Each diagnosis is a callable with ``check(ctx) -> Report`` and optional
``fix(ctx, report) -> Report``. Diagnoses register themselves at import time
via :func:`register`; the package's :mod:`diagnoses` subpackage auto-imports
all known diagnosis modules.
"""

from dataclasses import dataclass
from typing import Callable, Dict, List, Optional

from .report import Report, DoctorContext, Severity


[docs] @dataclass class Diagnosis: name: str description: str scope: str # 'global' or 'partition' check: Callable[[DoctorContext], Report] fix: Optional[Callable[[DoctorContext, Report], Report]] = None aliases: List[str] = None
_REGISTRY: Dict[str, Diagnosis] = {} # Group aliases users can pass to --check/--fix. Resolved at dispatch time # against whatever diagnoses have registered. ALIAS_GROUPS = { 'db': ['backfill', 'orphans', 'log_state', 'metadata', 'parquet_health', 'geoparquet_bbox'], 'soc': ['soc_health'], 'all': None, # special: all registered diagnoses }
[docs] def register(name: str, description: str, scope: str = 'global', fix: Optional[Callable] = None): """Decorator to register a check function. Usage:: @register('orphans', 'Leftover temp + empty partitions', scope='global', fix=orphans_fix) def orphans_check(ctx): ... """ if scope not in ('global', 'partition'): raise ValueError(f"scope must be 'global' or 'partition', got {scope!r}") def decorator(check_fn): _REGISTRY[name] = Diagnosis( name=name, description=description, scope=scope, check=check_fn, fix=fix, ) return check_fn return decorator
[docs] def get_diagnoses() -> Dict[str, Diagnosis]: """Return a copy of the diagnosis registry.""" return dict(_REGISTRY)
[docs] def resolve_names(names: List[str]) -> List[str]: """Expand alias groups (``db``, ``soc``, ``all``) into concrete names.""" if not names: return list(_REGISTRY.keys()) out = [] seen = set() for n in names: if n in ALIAS_GROUPS: members = ALIAS_GROUPS[n] if ALIAS_GROUPS[n] is not None else list(_REGISTRY.keys()) for m in members: if m in _REGISTRY and m not in seen: out.append(m) seen.add(m) elif n in _REGISTRY: if n not in seen: out.append(n) seen.add(n) else: raise ValueError( f"Unknown diagnosis '{n}'. Available: {sorted(_REGISTRY)} " f"or aliases: {sorted(ALIAS_GROUPS)}" ) return out
def _fused_check_reports(ctx: DoctorContext, names: List[str]) -> Dict[str, Report]: """Dispatch ``names`` as a single per-partition fused scan. Returns a ``{name: Report}`` map. Only diagnoses registered with the fused registry (``gedih3.doctor.fused.register_scan``) participate; the caller is responsible for filtering ``names`` accordingly. The single ``parallel_map`` opens each partition's parquet listing and meta JSON once and routes the result through every enabled scan, then each diagnosis's ``_finalize_*`` driver-side aggregator builds its Report. GPFS read amplification drops 3-5× on a continental DB vs. one ``parallel_map`` per diagnosis. """ from .fused import fused_scan_partition from .parallel import parallel_map from .diagnoses.backfill import ( _active_products, _expected_product_columns, _finalize_backfill_check, ) from .diagnoses.metadata import _finalize_metadata_check from .diagnoses.geoparquet_bbox import _finalize_geoparquet_bbox_check from .diagnoses.parquet_health import _finalize_parquet_health_check from .diagnoses.orphans import _finalize_orphans_check # Per-diagnosis broadcast kwargs. Computed once on the driver and # forwarded to the worker as a namespaced ``scan_kwargs`` dict. scan_kwargs: Dict[str, Dict] = {} if 'backfill' in names: products = _active_products(ctx) if products: scan_kwargs['backfill'] = { 'products': products, 'expected_by_product': {p: _expected_product_columns(ctx, p) for p in products}, } else: # No active products → backfill has nothing to do; drop from # the fused dispatch and return its no-op report directly. names = [n for n in names if n != 'backfill'] if 'orphans' in names: age_hours = getattr(ctx.args, 'orphan_age_hours', 24.0) scan_kwargs['orphans'] = {'age_seconds': age_hours * 3600} # Dispatch the fused scan over partition dirs. Each per-partition # result is a {scan_name: payload} dict. per_partition: Dict[str, Dict[str, object]] = {} for part_dir, result in parallel_map( ctx.partition_dirs, fused_scan_partition, args=getattr(ctx, 'args', None), desc='fused: scanning partitions', unit='part', enabled_scans=list(names), scan_kwargs=scan_kwargs, ): if isinstance(result, Exception): # Whole-partition failure: surface the same scan_error finding # each diagnosis would have produced individually. per_partition[part_dir] = {n: result for n in names} else: per_partition[part_dir] = result # Split per_partition into a {name: {part_dir: scan_result}} map for # each diagnosis's finalize step. by_name: Dict[str, Dict[str, object]] = {n: {} for n in names} for part_dir, scan_results in per_partition.items(): # ``scan_results`` is the per-partition dict the fused worker # returned (or a {n: Exception} dict if the whole task failed). # ``.get(n)`` is None when the worker skipped this scan because # the registry didn't have it — most commonly happens when the # cluster has stale bytecode predating this refactor. get = getattr(scan_results, 'get', None) for n in names: by_name[n][part_dir] = get(n) if get else None # Detect scans where EVERY partition returned None (i.e. fusion # produced no usable data for this diagnosis). Fall through to the # single-diagnosis dispatch in those cases so the operator gets a # correct report instead of a misleading "all errored" finding. from ..logging_config import get_logger _log = get_logger(__name__) failed_in_fusion = [] for n in list(names): if all(v is None for v in by_name[n].values()): failed_in_fusion.append(n) _log.warning( f"fused scan returned no results for {n!r} (cluster may have " f"stale bytecode); falling back to per-diagnosis dispatch" ) for n in failed_in_fusion: names.remove(n) reports: Dict[str, Report] = {} if 'metadata' in names: reports['metadata'] = _finalize_metadata_check(ctx, by_name['metadata']) if 'geoparquet_bbox' in names: reports['geoparquet_bbox'] = _finalize_geoparquet_bbox_check(ctx, by_name['geoparquet_bbox']) if 'parquet_health' in names: reports['parquet_health'] = _finalize_parquet_health_check(ctx, by_name['parquet_health']) if 'orphans' in names: reports['orphans'] = _finalize_orphans_check( ctx, by_name['orphans'], scan_kwargs['orphans']['age_seconds'] ) if 'backfill' in names: reports['backfill'] = _finalize_backfill_check(ctx, by_name['backfill']) return reports
[docs] def run_diagnoses(ctx: DoctorContext, names: List[str], mode: str = 'check') -> List[Report]: """Run the named diagnoses (or aliases) against ``ctx``. When two or more per-partition h3db diagnoses are requested at ``mode='check'``, dispatch fuses them into a single per-partition scan that opens each partition's files once and routes the per- partition payload to every enabled diagnosis's driver-side aggregator. Falls back to per-diagnosis dispatch for fix mode and for single-diagnosis check invocations. Parameters ---------- ctx : DoctorContext Shared state. names : list of str Diagnosis names or alias groups. Empty list = all registered. mode : str 'check' (read-only) or 'fix' (apply remedies where supported). Returns ------- list of Report """ if mode not in ('check', 'fix'): raise ValueError(f"mode must be 'check' or 'fix', got {mode!r}") resolved = resolve_names(names) reports = [] # Identify the subset of resolved diagnoses that are fusion-eligible. # Fusion only applies to ``mode='check'`` because each diagnosis's # fix flow has independent semantics (some rewrite parquet, some # delete dirs, some only log). fused_reports: Dict[str, Report] = {} if mode == 'check' and len(resolved) >= 2: from .fused import fused_eligible_names eligible = [n for n in resolved if n in fused_eligible_names()] if len(eligible) >= 2: try: fused_reports = _fused_check_reports(ctx, eligible) except Exception as e: # Fusion path failure must not block the single-path # fallback: log and proceed. from ..logging_config import get_logger get_logger(__name__).warning( f"fused dispatch failed: {type(e).__name__}: {e}; " f"falling back to per-diagnosis dispatch" ) fused_reports = {} for name in resolved: diag = _REGISTRY[name] if name in fused_reports: report = fused_reports[name] else: try: report = diag.check(ctx) except Exception as e: report = Report( name=name, severity=Severity.ERROR, summary=f"check failed: {type(e).__name__}: {e}", findings=[{'error': str(e), 'type': type(e).__name__}], ) reports.append(report) continue if mode == 'fix' and report.has_findings and diag.fix is not None: try: report = diag.fix(ctx, report) except Exception as e: # Preserve the check report but flag the fix failure. report.severity = Severity.ERROR report.summary = f"{report.summary} | fix failed: {type(e).__name__}: {e}" report.findings.append({'fix_error': str(e), 'type': type(e).__name__}) reports.append(report) return reports