Distinguish failed D-InSAR engine attempts

This commit is contained in:
2026-07-01 16:46:25 +08:00
parent ce052b909e
commit 23958dbafb
3 changed files with 167 additions and 4 deletions
+89 -3
View File
@@ -5,8 +5,9 @@ from typing import Any, Dict, Iterable, List, Optional
from sqlalchemy import or_, select from sqlalchemy import or_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from ..models import DinsarTaskItemORM, ResultProductORM from ..models import DinsarProductionRunItemORM, DinsarTaskItemORM, ResultProductORM
from ..utils import normalize_satellite_family from ..utils import normalize_satellite_family
@@ -102,11 +103,32 @@ def _product_status(product: ResultProductORM) -> str:
return "ready" return "ready"
def _status_timestamp(value: Optional[datetime]) -> float:
return _timestamp(value)
def _latest_failed_attempt(attempts: Iterable[DinsarProductionRunItemORM]) -> Optional[DinsarProductionRunItemORM]:
latest: Optional[DinsarProductionRunItemORM] = None
for attempt in attempts:
if str(attempt.status or "").strip().upper() != "FAILED":
continue
if latest is None or (
_status_timestamp(attempt.ended_at or attempt.updated_at or attempt.created_at),
attempt.id or 0,
) > (
_status_timestamp(latest.ended_at or latest.updated_at or latest.created_at),
latest.id or 0,
):
latest = attempt
return latest
def serialize_engine_result( def serialize_engine_result(
*, *,
engine_code: str, engine_code: str,
data_family: str, data_family: str,
product: Optional[ResultProductORM], product: Optional[ResultProductORM],
failed_attempt: Optional[DinsarProductionRunItemORM] = None,
allowed: bool, allowed: bool,
legacy: bool = False, legacy: bool = False,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
@@ -119,6 +141,10 @@ def serialize_engine_result(
status = "blocked" status = "blocked"
can_dispatch = False can_dispatch = False
skip_reason = "unsupported_data_family" skip_reason = "unsupported_data_family"
elif product is None and failed_attempt is not None:
status = "failed"
can_dispatch = True
skip_reason = "production_failed"
elif product is None: elif product is None:
status = "missing" status = "missing"
can_dispatch = True can_dispatch = True
@@ -140,11 +166,18 @@ def serialize_engine_result(
), ),
"latest_product_id": product.id if product is not None else None, "latest_product_id": product.id if product is not None else None,
"product_id": product.product_id if product is not None else None, "product_id": product.product_id if product is not None else None,
"run_key": product.run_key if product is not None else None, "run_key": product.run_key if product is not None else failed_attempt.latest_run_key if failed_attempt is not None else None,
"published_at": product.published_at if product is not None else None, "published_at": product.published_at if product is not None else None,
"health_status": product.health_status if product is not None else None, "health_status": product.health_status if product is not None else None,
"primary_asset_path": product.primary_asset_path if product is not None else None, "primary_asset_path": product.primary_asset_path if product is not None else None,
"preview_path": product.preview_path if product is not None else None, "preview_path": product.preview_path if product is not None else None,
"production_run_id": failed_attempt.run_id if failed_attempt is not None else None,
"production_item_id": failed_attempt.id if failed_attempt is not None else None,
"production_status": failed_attempt.status if failed_attempt is not None else None,
"production_error": failed_attempt.last_error if failed_attempt is not None else None,
"latest_log_path": failed_attempt.latest_log_path if failed_attempt is not None else None,
"latest_output_dir": failed_attempt.latest_output_dir if failed_attempt is not None else None,
"attempt_ended_at": failed_attempt.ended_at if failed_attempt is not None else None,
"can_dispatch": can_dispatch, "can_dispatch": can_dispatch,
"skip_reason": skip_reason, "skip_reason": skip_reason,
} }
@@ -153,6 +186,7 @@ def serialize_engine_result(
def build_engine_results( def build_engine_results(
*, *,
products: Iterable[ResultProductORM], products: Iterable[ResultProductORM],
failed_attempts: Iterable[DinsarProductionRunItemORM] = (),
data_family: str = "unknown", data_family: str = "unknown",
include_legacy: bool = False, include_legacy: bool = False,
) -> Dict[str, Dict[str, Any]]: ) -> Dict[str, Dict[str, Any]]:
@@ -168,6 +202,17 @@ def build_engine_results(
): ):
target[engine] = product target[engine] = product
failed_by_engine: Dict[str, DinsarProductionRunItemORM] = {}
attempts_by_engine: Dict[str, List[DinsarProductionRunItemORM]] = {}
for attempt in failed_attempts:
engine = normalize_dinsar_engine_code(getattr(getattr(attempt, "run", None), "engine_code", ""))
if engine:
attempts_by_engine.setdefault(engine, []).append(attempt)
for engine, attempts in attempts_by_engine.items():
latest = _latest_failed_attempt(attempts)
if latest is not None:
failed_by_engine[engine] = latest
allowed = allowed_engines_for_data_family(data_family) allowed = allowed_engines_for_data_family(data_family)
matrix: Dict[str, Dict[str, Any]] = {} matrix: Dict[str, Dict[str, Any]] = {}
for engine in CURRENT_DINSAR_ENGINE_ORDER: for engine in CURRENT_DINSAR_ENGINE_ORDER:
@@ -175,6 +220,7 @@ def build_engine_results(
engine_code=engine, engine_code=engine,
data_family=data_family, data_family=data_family,
product=latest_by_engine.get(engine), product=latest_by_engine.get(engine),
failed_attempt=failed_by_engine.get(engine) if latest_by_engine.get(engine) is None else None,
allowed=engine in allowed, allowed=engine in allowed,
) )
@@ -184,6 +230,7 @@ def build_engine_results(
engine_code=engine, engine_code=engine,
data_family=data_family, data_family=data_family,
product=product, product=product,
failed_attempt=None,
allowed=False, allowed=False,
legacy=True, legacy=True,
) )
@@ -229,6 +276,27 @@ async def build_engine_results_for_task_items(
) )
products = result.scalars().all() products = result.scalars().all()
attempt_conditions = []
if pair_keys:
attempt_conditions.append(DinsarProductionRunItemORM.pair_key.in_(pair_keys))
if aliases:
attempt_conditions.append(DinsarProductionRunItemORM.task_alias.in_(aliases))
attempt_conditions.append(DinsarProductionRunItemORM.task_name.in_(aliases))
failed_attempts: List[DinsarProductionRunItemORM] = []
if attempt_conditions:
attempt_result = await db.execute(
select(DinsarProductionRunItemORM)
.options(selectinload(DinsarProductionRunItemORM.run))
.where(DinsarProductionRunItemORM.status == "FAILED")
.where(or_(*attempt_conditions))
.order_by(
DinsarProductionRunItemORM.ended_at.desc().nullslast(),
DinsarProductionRunItemORM.updated_at.desc().nullslast(),
DinsarProductionRunItemORM.id.desc(),
)
)
failed_attempts = attempt_result.scalars().all()
by_pair_key: Dict[str, List[ResultProductORM]] = {} by_pair_key: Dict[str, List[ResultProductORM]] = {}
by_alias: Dict[str, List[ResultProductORM]] = {} by_alias: Dict[str, List[ResultProductORM]] = {}
for product in products: for product in products:
@@ -239,15 +307,33 @@ async def build_engine_results_for_task_items(
if alias: if alias:
by_alias.setdefault(alias, []).append(product) by_alias.setdefault(alias, []).append(product)
failed_by_pair_key: Dict[str, List[DinsarProductionRunItemORM]] = {}
failed_by_alias: Dict[str, List[DinsarProductionRunItemORM]] = {}
for attempt in failed_attempts:
pair_key = str(attempt.pair_key or "").strip()
if pair_key:
failed_by_pair_key.setdefault(pair_key, []).append(attempt)
for alias in {str(attempt.task_alias or "").strip(), str(attempt.task_name or "").strip()}:
if alias:
failed_by_alias.setdefault(alias, []).append(attempt)
output: Dict[int, Dict[str, Dict[str, Any]]] = {} output: Dict[int, Dict[str, Dict[str, Any]]] = {}
for item in items: for item in items:
item_products: List[ResultProductORM] = [] item_products: List[ResultProductORM] = []
item_failed_attempts: List[DinsarProductionRunItemORM] = []
pair_key = str(item.pair_key or "").strip() pair_key = str(item.pair_key or "").strip()
alias = str(item.task_alias or item.task_name or "").strip() alias = str(item.task_alias or item.task_name or "").strip()
if pair_key: if pair_key:
item_products.extend(by_pair_key.get(pair_key, [])) item_products.extend(by_pair_key.get(pair_key, []))
item_failed_attempts.extend(failed_by_pair_key.get(pair_key, []))
if not item_products and alias: if not item_products and alias:
item_products.extend(by_alias.get(alias, [])) item_products.extend(by_alias.get(alias, []))
if not item_failed_attempts and alias:
item_failed_attempts.extend(failed_by_alias.get(alias, []))
data_family = infer_dinsar_data_family(item.master_satellite, item.slave_satellite) data_family = infer_dinsar_data_family(item.master_satellite, item.slave_satellite)
output[int(item.id)] = build_engine_results(products=item_products, data_family=data_family) output[int(item.id)] = build_engine_results(
products=item_products,
failed_attempts=item_failed_attempts,
data_family=data_family,
)
return output return output
@@ -12,11 +12,13 @@ from typing import Any, Dict, Iterable, List, Optional, Tuple
from geoalchemy2.shape import from_shape from geoalchemy2.shape import from_shape
from sqlalchemy import delete, func, or_, select from sqlalchemy import delete, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from shapely.geometry import Polygon, shape from shapely.geometry import Polygon, shape
from ..config import settings from ..config import settings
from ..models import ( from ..models import (
DinsarProductProfileORM, DinsarProductProfileORM,
DinsarProductionRunItemORM,
DinsarTaskItemORM, DinsarTaskItemORM,
PairingMetricCacheORM, PairingMetricCacheORM,
PairingNetworkEdgeORM, PairingNetworkEdgeORM,
@@ -1519,8 +1521,16 @@ class ResultCatalogService:
products = result.scalars().all() products = result.scalars().all()
groups: Dict[str, Dict[str, Any]] = {} groups: Dict[str, Dict[str, Any]] = {}
pair_keys_for_trace = set()
aliases_for_trace = set()
for product in products: for product in products:
pair_key = str(product.pair_key or "").strip() or f"product:{product.id}" pair_key = str(product.pair_key or "").strip() or f"product:{product.id}"
if str(product.pair_key or "").strip():
pair_keys_for_trace.add(str(product.pair_key or "").strip())
for alias_value in (product.task_alias, product.task_name):
alias_text = str(alias_value or "").strip()
if alias_text:
aliases_for_trace.add(alias_text)
group = groups.setdefault( group = groups.setdefault(
pair_key, pair_key,
{ {
@@ -1552,11 +1562,49 @@ class ResultCatalogService:
if group.get(key) in (None, "") and getattr(product, key) not in (None, ""): if group.get(key) in (None, "") and getattr(product, key) not in (None, ""):
group[key] = getattr(product, key) group[key] = getattr(product, key)
failed_trace_by_pair_key: Dict[str, List[DinsarProductionRunItemORM]] = {}
failed_trace_by_alias: Dict[str, List[DinsarProductionRunItemORM]] = {}
trace_conditions = []
if pair_keys_for_trace:
trace_conditions.append(DinsarProductionRunItemORM.pair_key.in_(sorted(pair_keys_for_trace)))
if aliases_for_trace:
trace_conditions.append(DinsarProductionRunItemORM.task_alias.in_(sorted(aliases_for_trace)))
trace_conditions.append(DinsarProductionRunItemORM.task_name.in_(sorted(aliases_for_trace)))
if trace_conditions:
trace_result = await db.execute(
select(DinsarProductionRunItemORM)
.options(selectinload(DinsarProductionRunItemORM.run))
.where(DinsarProductionRunItemORM.status == "FAILED")
.where(or_(*trace_conditions))
.order_by(
DinsarProductionRunItemORM.ended_at.desc().nullslast(),
DinsarProductionRunItemORM.updated_at.desc().nullslast(),
DinsarProductionRunItemORM.id.desc(),
)
)
for attempt in trace_result.scalars().all():
pair_key = str(attempt.pair_key or "").strip()
if pair_key:
failed_trace_by_pair_key.setdefault(pair_key, []).append(attempt)
for alias_value in (attempt.task_alias, attempt.task_name):
alias_text = str(alias_value or "").strip()
if alias_text:
failed_trace_by_alias.setdefault(alias_text, []).append(attempt)
items: List[Dict[str, Any]] = [] items: List[Dict[str, Any]] = []
for group in groups.values(): for group in groups.values():
products_for_group: List[ResultProductORM] = group.pop("_products") products_for_group: List[ResultProductORM] = group.pop("_products")
failed_attempts_for_group: List[DinsarProductionRunItemORM] = []
pair_key = str(group.get("pair_key") or "").strip()
if pair_key:
failed_attempts_for_group.extend(failed_trace_by_pair_key.get(pair_key, []))
for alias_value in (group.get("task_alias"), group.get("task_name")):
alias_text = str(alias_value or "").strip()
if alias_text and not failed_attempts_for_group:
failed_attempts_for_group.extend(failed_trace_by_alias.get(alias_text, []))
engine_results = build_engine_results( engine_results = build_engine_results(
products=products_for_group, products=products_for_group,
failed_attempts=failed_attempts_for_group,
data_family=group.get("data_family") or "unknown", data_family=group.get("data_family") or "unknown",
include_legacy=include_legacy, include_legacy=include_legacy,
) )
+30 -1
View File
@@ -67,6 +67,34 @@ function engineStatusTone(status) {
return 'neutral'; return 'neutral';
} }
function formatEngineResultStatus(status) {
const normalized = String(status || 'missing').toLowerCase();
if (normalized === 'ready') return '已生产';
if (normalized === 'failed') return '生产失败';
if (normalized === 'missing') return '未生产';
if (normalized === 'blocked') return '不适用';
if (normalized === 'running') return '生产中';
if (normalized === 'legacy') return '历史结果';
return status || '未生产';
}
function buildEngineResultTitle(result = {}) {
const parts = [formatEngineResultStatus(result.status)];
if (result.skip_reason === 'production_failed') {
parts.push('该引擎已有失败生产记录,不是单纯缺失结果。');
}
if (result.production_run_id) {
parts.push(`run=${result.production_run_id}`);
}
if (result.production_error) {
parts.push(String(result.production_error).replace(/\s+/g, ' ').slice(0, 220));
}
if (result.latest_log_path) {
parts.push(`log=${result.latest_log_path}`);
}
return parts.filter(Boolean).join('\n');
}
function MetaField({ label, value, multiline = false }) { function MetaField({ label, value, multiline = false }) {
const displayValue = value === null || value === undefined || value === '' ? '-' : value; const displayValue = value === null || value === undefined || value === '' ? '-' : value;
return ( return (
@@ -373,8 +401,9 @@ export default function DinsarCatalogPanel({
<span <span
key={engineCode} key={engineCode}
className={`dinsar-engine-result-chip tone-${engineStatusTone(result.status)}`} className={`dinsar-engine-result-chip tone-${engineStatusTone(result.status)}`}
title={buildEngineResultTitle(result)}
> >
{engineMeta.shortLabel}: {result.status || 'missing'} {engineMeta.shortLabel}: {formatEngineResultStatus(result.status)}
</span> </span>
); );
})} })}