Files
getDomain/domain-api/app/services/detect_job_service.py

4547 lines
192 KiB
Python
Raw Permalink 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.
from __future__ import annotations
import hashlib
import math
import json
import threading
import time
from datetime import datetime, timedelta
from uuid import uuid4
from psycopg2 import errors
from app.core.config import settings
from app.core.db import db_read_retry, get_db, is_retryable_db_error
from app.services.debug_event_service import push_debug_event
from app.services.settings_service import get_settings_payload
ACTIVE_JOB_STATUSES = ("pending", "running")
_RUNTIME_NODE_STALE_MINUTES = 10
_PREFERRED_RUNTIME_SNAPSHOT_GRACE = timedelta(minutes=10)
_LIVE_RUNTIME_NODE_STALE_AFTER = timedelta(seconds=90)
_DEFAULT_TASK_MODE = "domain_pipeline"
_DETECT_JOBS_LEGACY_SELECT_MODE: bool | None = None
_DETECT_JOB_ITEM_RECYCLE_LOCK = threading.Lock()
_DETECT_JOB_ITEM_RECYCLE_LAST_RUN_TS = 0.0
_DETECT_JOB_ITEM_RECYCLE_INTERVAL_SECONDS = 15.0
_DETECT_JOB_ITEM_RECYCLE_ADVISORY_LOCK_KEY = 0
_DETECT_JOB_ITEM_RECYCLE_BATCH_SIZE = 2000
_DETECT_JOB_ITEM_RECYCLE_MAX_BATCHES = 6
_ACTIVE_JOB_SUMMARY_CACHE_LOCK = threading.Lock()
_ACTIVE_JOB_SUMMARY_CACHE_TTL_SECONDS = 3.0
_ACTIVE_JOB_SUMMARY_CACHE: dict[tuple[int], tuple[float, dict | None]] = {}
_DETECT_QUEUE_HEALTH_CACHE_LOCK = threading.Lock()
_DETECT_QUEUE_HEALTH_CACHE_TTL_SECONDS = 3.0
_DETECT_QUEUE_HEALTH_CACHE: dict[tuple[int], tuple[float, dict]] = {}
_DISABLED_MANAGED_NODE_CACHE: set[str] = set()
_DISABLED_MANAGED_NODE_CACHE_EXPIRES_AT = 0.0
_PIPELINE_STEP_ORDER = [
"detect_register",
"detect_baidu_site",
"detect_360_site",
"detect_chinaz",
"detect_aizhan",
"detect_wayback",
"detect_jucha",
"detect_juziseo",
]
_PIPELINE_STEP_FIELDS = {
"detect_baidu_site": "baidu_site",
"detect_360_site": "qihu360_site",
"detect_chinaz": "chinaz_info",
"detect_aizhan": "aizhan_info",
"detect_wayback": "wayback_info",
"detect_jucha": "jucha_info",
"detect_juziseo": "juziseo_info",
}
_PIPELINE_STEP_LABELS = {
"detect_register": "注册状态检测",
"detect_baidu_site": "百度site检测",
"detect_360_site": "360 site检测",
"detect_chinaz": "站长之家检测",
"detect_aizhan": "爱站检测",
"detect_wayback": "时光机检测",
"detect_jucha": "聚查检测",
"detect_juziseo": "桔子检测",
}
_PIPELINE_STEP_RETRY_LIMITS = {
"default": 3,
"detect_wayback": 2,
}
_SINGLE_STEP_JOB_SPECS = {
"detect_baidu_site": {
"step_code": "detect_baidu_site",
"task_mode": "single_step",
"label": "百度site检测",
"source": "api-step-start",
},
"detect_wayback": {
"step_code": "detect_wayback",
"task_mode": "single_step",
"label": "时光机检测",
"source": "api-step-start",
},
}
def _build_pg_advisory_lock_key(scope: str) -> int:
normalized_scope = str(scope or "").strip() or "domaincheck-default"
digest = hashlib.sha1(normalized_scope.encode("utf-8")).digest()
raw_value = int.from_bytes(digest[:8], "big", signed=False)
return raw_value - (1 << 64) if raw_value >= (1 << 63) else raw_value
_DETECT_JOB_ITEM_RECYCLE_ADVISORY_LOCK_KEY = _build_pg_advisory_lock_key("detect-job-items-recycle-expired")
def _clone_cacheable_payload(value):
try:
return json.loads(json.dumps(value, ensure_ascii=False))
except Exception:
if isinstance(value, dict):
return dict(value)
if isinstance(value, list):
return list(value)
return value
def _selection_sql() -> str:
return """
SELECT id
FROM domains
WHERE
detect_status IN (0, 4)
OR (use_status = 0 AND detect_status = 1 AND register_status = 3 AND expire_date < CURRENT_DATE)
ORDER BY id ASC
LIMIT %s
"""
def _refresh_detect_job_status_with_cursor(cur, job_id: int) -> None:
cur.execute(
"""
SELECT
job.task_mode,
count(*) FILTER (WHERE item.status = 'pending') AS pending_count,
count(*) FILTER (WHERE item.status IN ('claimed', 'running')) AS dispatch_active_count,
count(*) FILTER (WHERE item.status = 'failed') AS failed_count,
count(*) FILTER (WHERE item.status IN ('completed', 'blacklisted')) AS done_count,
count(*) FILTER (
WHERE item.status IN ('completed', 'blacklisted', 'failed')
AND COALESCE(item.step_code, '') <> ''
AND COALESCE(item.result_payload_json->>'controller_processed', 'false') <> 'true'
) AS unprocessed_terminal_count
FROM detect_job_items AS item
JOIN detect_jobs AS job ON job.id = item.job_id
WHERE item.job_id = %s
GROUP BY job.task_mode
""",
(int(job_id),),
)
refresh_row = cur.fetchone()
if not refresh_row:
return
task_mode = str((refresh_row or [""])[0] or "").strip()
pending_count = int((refresh_row or ["", 0])[1] or 0)
dispatch_active_count = int((refresh_row or ["", 0, 0])[2] or 0)
failed_count = int((refresh_row or ["", 0, 0, 0])[3] or 0)
done_count = int((refresh_row or ["", 0, 0, 0, 0])[4] or 0)
unprocessed_terminal_count = int((refresh_row or ["", 0, 0, 0, 0, 0])[5] or 0)
if dispatch_active_count > 0 or (task_mode == "domain_pipeline" and unprocessed_terminal_count > 0):
cur.execute(
"""
UPDATE detect_jobs
SET status = 'running',
started_at = COALESCE(started_at, CURRENT_TIMESTAMP),
finished_at = NULL
WHERE id = %s
AND (
status <> 'running'
OR started_at IS NULL
OR finished_at IS NOT NULL
)
""",
(int(job_id),),
)
return
if pending_count > 0:
cur.execute(
"""
UPDATE detect_jobs
SET status = 'pending',
finished_at = NULL
WHERE id = %s
""",
(int(job_id),),
)
return
final_status = "completed"
if failed_count > 0 and done_count > 0:
final_status = "partial_failed"
elif failed_count > 0:
final_status = "failed"
cur.execute(
"""
UPDATE detect_jobs
SET status = %s,
finished_at = CURRENT_TIMESTAMP,
started_at = COALESCE(started_at, CURRENT_TIMESTAMP)
WHERE id = %s
""",
(final_status, int(job_id)),
)
def _recycle_expired_detect_job_items_once() -> int:
conn = None
advisory_locked = False
try:
with get_db() as conn:
conn.autocommit = False
with conn.cursor() as cur:
cur.execute(
"SELECT pg_try_advisory_lock(%s)",
(_DETECT_JOB_ITEM_RECYCLE_ADVISORY_LOCK_KEY,),
)
lock_row = cur.fetchone()
advisory_locked = bool((lock_row or [False])[0])
if not advisory_locked:
conn.rollback()
return 0
recycled_count = 0
touched_job_ids: set[int] = set()
for _ in range(_DETECT_JOB_ITEM_RECYCLE_MAX_BATCHES):
cur.execute(
"""
WITH expired_candidates AS (
SELECT id, job_id, status
FROM detect_job_items
WHERE status IN ('claimed', 'running')
AND lease_expires_at IS NOT NULL
AND lease_expires_at < CURRENT_TIMESTAMP
ORDER BY lease_expires_at ASC, id ASC
FOR UPDATE SKIP LOCKED
LIMIT %s
),
recycled AS (
UPDATE detect_job_items AS item
SET status = 'pending',
claimed_by = '',
claim_token = '',
lease_expires_at = NULL,
updated_at = CURRENT_TIMESTAMP,
last_error = CASE
WHEN expired_candidates.status = 'running' THEN 'lease expired while running'
WHEN expired_candidates.status = 'claimed' THEN 'lease expired before running'
ELSE item.last_error
END
FROM expired_candidates
WHERE item.id = expired_candidates.id
RETURNING expired_candidates.job_id
)
SELECT count(*), array_remove(array_agg(DISTINCT job_id), NULL)
FROM recycled
""",
(_DETECT_JOB_ITEM_RECYCLE_BATCH_SIZE,),
)
row = cur.fetchone()
batch_count = int((row or [0])[0] or 0)
recycled_count += batch_count
touched_job_ids.update(
int(item)
for item in list((row or [0, []])[1] or [])
if item is not None
)
if batch_count < _DETECT_JOB_ITEM_RECYCLE_BATCH_SIZE:
break
for job_id in sorted(touched_job_ids):
_refresh_detect_job_status_with_cursor(cur, job_id)
conn.commit()
if recycled_count > 0:
try:
push_debug_event(
service="detect-job",
event_type="expired_item_recycled",
level="info",
message=f"控制面回收过期检测任务项 {recycled_count}",
payload={
"recycled_count": recycled_count,
"job_ids": touched_job_ids,
},
)
except Exception:
pass
return recycled_count
except Exception as exc:
try:
if conn is not None:
conn.rollback()
except Exception:
pass
try:
push_debug_event(
service="detect-job",
event_type="expired_item_recycle_failed",
level="warning",
message="控制面回收过期检测任务项失败",
payload={"error": str(exc or "")[:500]},
)
except Exception:
pass
return 0
finally:
if conn is not None and advisory_locked:
try:
with conn.cursor() as cur:
cur.execute(
"SELECT pg_advisory_unlock(%s)",
(_DETECT_JOB_ITEM_RECYCLE_ADVISORY_LOCK_KEY,),
)
conn.commit()
except Exception:
try:
conn.rollback()
except Exception:
pass
def _maybe_recycle_expired_detect_job_items() -> int:
global _DETECT_JOB_ITEM_RECYCLE_LAST_RUN_TS
now_ts = time.monotonic()
with _DETECT_JOB_ITEM_RECYCLE_LOCK:
if now_ts - _DETECT_JOB_ITEM_RECYCLE_LAST_RUN_TS < _DETECT_JOB_ITEM_RECYCLE_INTERVAL_SECONDS:
return 0
_DETECT_JOB_ITEM_RECYCLE_LAST_RUN_TS = now_ts
return _recycle_expired_detect_job_items_once()
def _format_time(value: datetime | None) -> str:
return value.isoformat(sep=" ", timespec="seconds") if value else ""
def _load_disabled_managed_node_codes(node_codes: list[str] | None = None) -> set[str]:
global _DISABLED_MANAGED_NODE_CACHE, _DISABLED_MANAGED_NODE_CACHE_EXPIRES_AT
normalized_codes = [
str(item or "").strip()
for item in list(node_codes or [])
if str(item or "").strip()
]
now_ts = time.time()
if not normalized_codes and now_ts < _DISABLED_MANAGED_NODE_CACHE_EXPIRES_AT:
return set(_DISABLED_MANAGED_NODE_CACHE)
try:
with get_db() as conn:
with conn.cursor() as cur:
if normalized_codes:
cur.execute(
"""
SELECT node_code
FROM ops_managed_nodes
WHERE is_enabled = FALSE
AND node_code = ANY(%s)
""",
(normalized_codes,),
)
else:
cur.execute(
"""
SELECT node_code
FROM ops_managed_nodes
WHERE is_enabled = FALSE
"""
)
rows = list(cur.fetchall() or [])
except Exception:
if normalized_codes:
return {code for code in normalized_codes if code in _DISABLED_MANAGED_NODE_CACHE}
return set(_DISABLED_MANAGED_NODE_CACHE)
disabled_codes = {
str(row[0] or "").strip()
for row in rows
if str(row[0] or "").strip()
}
if normalized_codes:
return disabled_codes
_DISABLED_MANAGED_NODE_CACHE = disabled_codes
_DISABLED_MANAGED_NODE_CACHE_EXPIRES_AT = now_ts + 5.0
return set(disabled_codes)
def _detect_jobs_select_columns(*, alias: str = "", legacy_mode: bool = False) -> str:
prefix = f"{str(alias).strip()}." if str(alias or "").strip() else ""
task_mode_expr = f"'{_DEFAULT_TASK_MODE}' AS task_mode" if legacy_mode else f"{prefix}task_mode"
step_code_expr = "'' AS step_code" if legacy_mode else f"{prefix}step_code"
return (
f"{prefix}id, {prefix}job_code, {prefix}source, "
f"{task_mode_expr}, {step_code_expr}, "
f"{prefix}status, {prefix}created_by, {prefix}created_at, {prefix}started_at, {prefix}finished_at"
)
def _execute_detect_jobs_select(
cur,
*,
from_clause: str,
where_clause: str = "",
order_clause: str = "",
limit_clause: str = "",
params: tuple | list | None = None,
alias: str = "",
) -> None:
global _DETECT_JOBS_LEGACY_SELECT_MODE
safe_params = tuple(params or ())
candidate_modes = (
[_DETECT_JOBS_LEGACY_SELECT_MODE]
if _DETECT_JOBS_LEGACY_SELECT_MODE is not None
else [False, True]
)
last_error: Exception | None = None
for legacy_mode in candidate_modes:
parts = [
f"SELECT {_detect_jobs_select_columns(alias=alias, legacy_mode=bool(legacy_mode))}",
from_clause,
]
if where_clause:
parts.append(f"WHERE {where_clause}")
if order_clause:
parts.append(f"ORDER BY {order_clause}")
if limit_clause:
normalized_limit = str(limit_clause or "").strip()
if normalized_limit:
parts.append(
normalized_limit
if normalized_limit.upper().startswith("LIMIT")
else f"LIMIT {normalized_limit}"
)
sql = "\n".join(parts)
try:
cur.execute(sql, safe_params)
_DETECT_JOBS_LEGACY_SELECT_MODE = bool(legacy_mode)
return
except errors.UndefinedColumn as exc:
last_error = exc
_DETECT_JOBS_LEGACY_SELECT_MODE = True
if bool(legacy_mode):
raise
if last_error is not None:
raise last_error
def _normalize_sync_region(value: str | None, fallback: str) -> str:
text = str(value or "").strip()
if not text or text == "unknown":
return str(fallback or "unknown").strip() or "unknown"
return text
def normalize_detect_step_code(step_code: object) -> str:
normalized = str(step_code or "").strip()
if not normalized:
return ""
return normalized if normalized in _SINGLE_STEP_JOB_SPECS else ""
def resolve_detect_job_definition(step_code: object = None) -> dict:
normalized_step_code = normalize_detect_step_code(step_code)
if normalized_step_code:
spec = dict(_SINGLE_STEP_JOB_SPECS[normalized_step_code])
spec["is_single_step"] = True
return spec
return {
"step_code": "",
"task_mode": _DEFAULT_TASK_MODE,
"label": "全流程检测",
"source": "api-start",
"is_single_step": False,
}
def _step_label(step_code: str) -> str:
return _PIPELINE_STEP_LABELS.get(str(step_code or "").strip(), str(step_code or "").strip() or "unknown")
def _json_state(value: object) -> str:
if isinstance(value, dict):
return str(value.get("state") or "").strip().lower()
return ""
def _pipeline_retry_limit(step_code: str) -> int:
return int(_PIPELINE_STEP_RETRY_LIMITS.get(str(step_code or "").strip(), _PIPELINE_STEP_RETRY_LIMITS["default"]) or 1)
def _classify_pipeline_item_outcome(
*,
item_status: str,
result_payload: dict | None,
step_code: str,
attempt_count: int,
) -> dict:
normalized_item_status = str(item_status or "").strip().lower()
payload = dict(result_payload or {})
result_state = str(payload.get("state") or "").strip().lower()
retry_limit = _pipeline_retry_limit(step_code)
retry_recommended = bool(payload.get("retry_recommended"))
legacy_retry_candidate = normalized_item_status == "failed" and not result_state
if normalized_item_status == "blacklisted" or result_state == "blacklisted":
return {
"action": "black_hit",
"result_state": result_state or "blacklisted",
"retry_limit": retry_limit,
"should_retry": False,
"reason_code": "blacklisted",
}
should_retry = (
result_state in {"degraded", "error"}
or retry_recommended
or legacy_retry_candidate
)
if should_retry:
return {
"action": "retry" if int(attempt_count or 0) < retry_limit else "reject",
"result_state": result_state or ("degraded" if retry_recommended else "failed"),
"retry_limit": retry_limit,
"should_retry": int(attempt_count or 0) < retry_limit,
"reason_code": "external_retry" if int(attempt_count or 0) < retry_limit else "retry_exhausted",
}
if normalized_item_status == "failed" or result_state in {"failed", "rejected", "reject"}:
return {
"action": "reject",
"result_state": result_state or "failed",
"retry_limit": retry_limit,
"should_retry": False,
"reason_code": "business_reject",
}
return {
"action": "pass",
"result_state": result_state or "passed",
"retry_limit": retry_limit,
"should_retry": False,
"reason_code": "passed",
}
def _load_pipeline_order(settings_payload: dict | None = None) -> list[str]:
payload = settings_payload or get_settings_payload()
detect_options = dict(payload.get("detect_options") or {})
configured_order = list(detect_options.get("detect_order") or [])
normalized_order = [item for item in configured_order if item in _PIPELINE_STEP_ORDER]
for item in _PIPELINE_STEP_ORDER:
if item not in normalized_order:
normalized_order.append(item)
return [item for item in normalized_order if bool(detect_options.get(item, False))]
def order_step_buckets(
step_items: list[dict] | None,
*,
settings_payload: dict | None = None,
limit: int | None = None,
) -> list[dict]:
normalized_items = [dict(item) for item in list(step_items or []) if isinstance(item, dict)]
if not normalized_items:
return []
pipeline_order = _load_pipeline_order(settings_payload=settings_payload)
order_index = {step_code: index for index, step_code in enumerate(pipeline_order)}
ordered = sorted(
normalized_items,
key=lambda item: (
int(order_index.get(str(item.get("step_code") or "").strip(), 10_000)),
str(item.get("step_code") or ""),
),
)
if limit is not None and int(limit or 0) > 0:
return ordered[: int(limit)]
return ordered
def _load_domain_pipeline_snapshot(cur, domain_id: int) -> dict | None:
cur.execute(
"""
SELECT
d.id,
d.domain,
d.source_type,
d.register_status,
d.detect_status,
d.use_status,
d.expire_date,
dd.baidu_site,
dd.qihu360_site,
dd.wayback_info,
dd.chinaz_info,
dd.aizhan_info,
dd.jucha_info,
dd.juziseo_info
FROM domains AS d
LEFT JOIN domain_detections AS dd
ON dd.domain_id = d.id
WHERE d.id = %s
LIMIT 1
""",
(int(domain_id),),
)
row = cur.fetchone()
if not row:
return None
return {
"id": int(row[0]),
"domain": row[1] or "",
"source_type": int(row[2] or 0),
"register_status": int(row[3] or 0),
"detect_status": int(row[4] or 0),
"use_status": int(row[5] or 0),
"expire_date": _format_time(row[6]) if row[6] else "",
"baidu_site": _decode_payload(row[7]),
"qihu360_site": _decode_payload(row[8]),
"wayback_info": _decode_payload(row[9]),
"chinaz_info": _decode_payload(row[10]),
"aizhan_info": _decode_payload(row[11]),
"jucha_info": _decode_payload(row[12]),
"juziseo_info": _decode_payload(row[13]),
}
def _is_pipeline_step_completed(domain_snapshot: dict, step_code: str) -> bool:
normalized_step_code = str(step_code or "").strip()
if normalized_step_code == "detect_register":
return int(domain_snapshot.get("register_status", 0) or 0) not in {0, 10}
field_name = _PIPELINE_STEP_FIELDS.get(normalized_step_code)
if not field_name:
return False
state = _json_state(domain_snapshot.get(field_name))
return state in {"passed", "blacklisted"}
def _should_skip_pipeline_step(domain_snapshot: dict, step_code: str) -> bool:
normalized_step_code = str(step_code or "").strip()
if normalized_step_code == "detect_register" and int(domain_snapshot.get("source_type", 0) or 0) == 1:
return True
return False
def resolve_domain_pipeline_step(domain_snapshot: dict, *, settings_payload: dict | None = None, after_step_code: str | None = None) -> str:
pipeline_order = _load_pipeline_order(settings_payload=settings_payload)
if not pipeline_order:
return ""
normalized_after = str(after_step_code or "").strip()
seen_after = not normalized_after
for step_code in pipeline_order:
if not seen_after:
if step_code == normalized_after:
seen_after = True
continue
if _should_skip_pipeline_step(domain_snapshot, step_code):
continue
if _is_pipeline_step_completed(domain_snapshot, step_code):
continue
return step_code
return ""
def resolve_initial_domain_pipeline_item(
domain_snapshot: dict,
*,
settings_payload: dict | None = None,
retry_count: int = 0,
) -> tuple[str, dict | None]:
step_code = resolve_domain_pipeline_step(domain_snapshot, settings_payload=settings_payload)
if not step_code:
return "", None
return step_code, _build_step_payload(
step_code=step_code,
domain_snapshot=domain_snapshot,
settings_payload=settings_payload,
retry_count=retry_count,
)
def _build_step_payload(*, step_code: str, domain_snapshot: dict, settings_payload: dict | None = None, retry_count: int = 0) -> dict:
pipeline_order = _load_pipeline_order(settings_payload=settings_payload)
payload = {
"step_code": str(step_code or "").strip(),
"step_name": _step_label(step_code),
"pipeline_order": pipeline_order,
"retry_count": int(retry_count or 0),
"domain": str(domain_snapshot.get("domain") or "").strip(),
"source_type": int(domain_snapshot.get("source_type", 0) or 0),
}
if str(step_code or "").strip() == "detect_wayback":
payload.update(
{
"wayback_strategy": "recent_years",
"wayback_recent_years": 5,
"wayback_stop_on_first_hit": True,
}
)
return payload
def _decode_payload(value: object) -> dict:
if isinstance(value, dict):
return value
if value in (None, ""):
return {}
try:
return json.loads(value)
except Exception:
return {}
def _upsert_domain_detection_field(cur, domain_id: int, field_name: str, payload: dict) -> bool:
normalized_field_name = str(field_name or "").strip()
if not normalized_field_name or normalized_field_name not in _PIPELINE_STEP_FIELDS.values():
return False
normalized_payload = dict(payload or {})
cur.execute("SELECT id FROM domain_detections WHERE domain_id = %s LIMIT 1", (int(domain_id),))
existing = cur.fetchone()
payload_json = json.dumps(normalized_payload, ensure_ascii=False)
if existing:
cur.execute(
f"""
UPDATE domain_detections
SET {normalized_field_name} = %s::jsonb,
update_time = NOW()
WHERE domain_id = %s
""",
(payload_json, int(domain_id)),
)
return True
cur.execute(
f"""
INSERT INTO domain_detections (domain_id, {normalized_field_name}, create_time, update_time)
VALUES (%s, %s::jsonb, NOW(), NOW())
""",
(int(domain_id), payload_json),
)
return True
def _int_value(value: object) -> int:
try:
return int(value or 0)
except Exception:
return 0
def _max_runtime_metric(*values: object) -> int:
return max((_int_value(value) for value in values), default=0)
def _effective_runtime_load(*, items_running: object, active_threads: object) -> int:
return max(_int_value(items_running), _int_value(active_threads), 0)
def _control_node_supports_worker(*, region: object, metadata: dict | None) -> bool:
normalized_region = str(region or "").strip()
runtime_metadata = dict(metadata or {})
active_threads = int(runtime_metadata.get("active_threads", 0) or 0)
max_threads = int(runtime_metadata.get("max_threads", 0) or 0)
if normalized_region != "mainland":
return False
return bool(
runtime_metadata.get("worker_online", False)
or runtime_metadata.get("detect_participating", False)
or active_threads > 0
or max_threads > 0
)
def _build_runtime_display_bucket(row: tuple) -> dict | None:
node_code = str(row[0] or "").strip()
if not node_code:
return None
region = str(row[1] or "").strip()
role = str(row[2] or "").strip()
metadata = _decode_payload(row[5])
if role == "control" and not _control_node_supports_worker(region=region, metadata=metadata):
return None
items_total = _int_value(metadata.get("job_items_total"))
items_claimed = _int_value(metadata.get("job_items_claimed"))
items_running = _int_value(metadata.get("job_items_running"))
items_completed = _int_value(metadata.get("job_items_completed"))
items_failed = _int_value(metadata.get("job_items_failed"))
raw_current_load = _int_value(row[4])
active_threads = _int_value(metadata.get("active_threads"))
max_threads = _int_value(metadata.get("max_threads"))
current_load = _effective_runtime_load(items_running=items_running, active_threads=active_threads)
detect_participating = bool(metadata.get("detect_participating", False))
if (
settings.node_region == "overseas"
and settings.node_role == "control"
and node_code == str(settings.node_code or "").strip()
and not detect_participating
):
# Overseas control nodes may report their own control-plane load into
# detect_worker_nodes; that load should never be counted as worker execution.
return None
if (
items_total <= 0
and items_claimed <= 0
and items_running <= 0
and items_completed <= 0
and items_failed <= 0
and current_load <= 0
and raw_current_load <= 0
and active_threads <= 0
and not detect_participating
):
return None
items_pending = max(items_total - items_claimed - items_running - items_completed - items_failed, 0)
return {
"node_code": node_code,
"items_total": items_total,
"items_pending": items_pending,
"items_claimed": items_claimed,
"items_running": items_running,
"display_running": current_load,
"items_completed": items_completed,
"items_blacklisted": 0,
"items_failed": items_failed,
"metrics_source": str(metadata.get("service") or "runtime").strip() or "runtime",
"region": region,
"role": role,
"status": str(row[3] or "").strip(),
"current_load": current_load,
"active_threads": active_threads,
"max_threads": max_threads,
"last_heartbeat_at": _format_time(row[6]) if row[6] else "",
}
def _merge_display_node_stats(*, local_node_stats: list[dict], runtime_node_rows: list[tuple]) -> list[dict]:
merged: dict[str, dict] = {}
def _ensure_bucket(node_code: str) -> dict:
return merged.setdefault(
node_code,
{
"node_code": node_code,
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"display_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
"metrics_source": "",
"region": "",
"role": "",
"status": "",
"current_load": 0,
"active_threads": 0,
"max_threads": 0,
"last_heartbeat_at": "",
},
)
for item in list(local_node_stats or []):
node_code = str(item.get("node_code") or "").strip()
if not node_code:
continue
bucket = _ensure_bucket(node_code)
for key in (
"items_total",
"items_pending",
"items_claimed",
"items_running",
"display_running",
"items_completed",
"items_blacklisted",
"items_failed",
):
bucket[key] = max(_int_value(bucket.get(key)), _int_value(item.get(key)))
if node_code == "unassigned":
bucket["metrics_source"] = "central_queue"
for row in list(runtime_node_rows or []):
runtime_bucket = _build_runtime_display_bucket(row)
if not runtime_bucket:
continue
node_code = str(runtime_bucket.get("node_code") or "").strip()
bucket = _ensure_bucket(node_code)
for key in (
"items_total",
"items_pending",
"items_claimed",
"items_running",
"display_running",
"items_completed",
"items_blacklisted",
"items_failed",
"current_load",
"active_threads",
"max_threads",
):
bucket[key] = max(_int_value(bucket.get(key)), _int_value(runtime_bucket.get(key)))
for key in ("metrics_source", "region", "role", "status", "last_heartbeat_at"):
if str(runtime_bucket.get(key) or "").strip():
bucket[key] = runtime_bucket.get(key)
return sorted(
merged.values(),
key=lambda item: (
str(item.get("node_code") or "") == "unassigned",
-_int_value(item.get("active_threads")),
-_int_value(item.get("items_running")),
-_int_value(item.get("items_claimed")),
-_int_value(item.get("items_total")),
str(item.get("node_code") or ""),
),
)
def _load_runtime_display_rows(cur) -> list[tuple]:
disabled_node_codes = _load_disabled_managed_node_codes()
cur.execute(
f"""
SELECT node_code, region, role, status, current_load, metadata_json, last_heartbeat_at
FROM detect_worker_nodes
WHERE last_heartbeat_at >= CURRENT_TIMESTAMP - interval '{_RUNTIME_NODE_STALE_MINUTES} minutes'
ORDER BY last_heartbeat_at DESC, node_code ASC
"""
)
return [
row
for row in list(cur.fetchall() or [])
if str((row or [""])[0] or "").strip() not in disabled_node_codes
]
def _build_display_summary(node_stats: list[dict]) -> dict:
effective_nodes = [
item
for item in list(node_stats or [])
if str(item.get("node_code") or "").strip()
and str(item.get("node_code") or "").strip() != "unassigned"
and not (
settings.node_region == "overseas"
and settings.node_role == "control"
and str(item.get("node_code") or "").strip() == str(settings.node_code or "").strip()
)
]
return {
"items_claimed": sum(_int_value(item.get("items_claimed")) for item in effective_nodes),
"items_running": sum(_int_value(item.get("items_running")) for item in effective_nodes),
"display_running": sum(
_effective_runtime_load(
items_running=item.get("items_running"),
active_threads=item.get("active_threads"),
)
for item in effective_nodes
),
"current_load": sum(
_effective_runtime_load(
items_running=item.get("items_running"),
active_threads=item.get("active_threads"),
)
for item in effective_nodes
),
"active_threads": sum(_int_value(item.get("active_threads")) for item in effective_nodes),
"max_threads": sum(_int_value(item.get("max_threads")) for item in effective_nodes),
"items_completed": sum(_int_value(item.get("items_completed")) for item in effective_nodes),
"items_failed": sum(_int_value(item.get("items_failed")) for item in effective_nodes),
"active_nodes": [
str(item.get("node_code") or "").strip()
for item in effective_nodes
if _effective_runtime_load(
items_running=item.get("items_running"),
active_threads=item.get("active_threads"),
) > 0
],
}
def _normalize_node_bucket(item: dict) -> dict:
node_code = str(item.get("node_code") or "").strip()
items_running = _int_value(item.get("items_running"))
active_threads = _int_value(item.get("active_threads"))
max_threads = _int_value(item.get("max_threads"))
current_load = _effective_runtime_load(items_running=items_running, active_threads=active_threads)
return {
"node_code": node_code,
"items_total": _int_value(item.get("items_total")),
"items_pending": _int_value(item.get("items_pending")),
"items_claimed": _int_value(item.get("items_claimed")),
"items_running": items_running,
"display_running": current_load,
"items_completed": _int_value(item.get("items_completed")),
"items_blacklisted": _int_value(item.get("items_blacklisted")),
"items_failed": _int_value(item.get("items_failed")),
"metrics_source": str(item.get("metrics_source") or "").strip(),
"region": str(item.get("region") or "").strip(),
"role": str(item.get("role") or "").strip(),
"status": str(item.get("status") or "").strip(),
"current_load": current_load,
"active_threads": active_threads,
"max_threads": max_threads,
"last_heartbeat_at": str(item.get("last_heartbeat_at") or "").strip(),
}
def _build_effective_node_stats(
*,
distributed_node_stats: list[dict],
raw_items_total: int,
) -> list[dict]:
normalized_total = max(0, _int_value(raw_items_total))
assigned_buckets: list[dict] = []
assigned_total = 0
unassigned_bucket: dict | None = None
for item in list(distributed_node_stats or []):
bucket = _normalize_node_bucket(item)
if not bucket["node_code"]:
continue
if bucket["node_code"] == "unassigned":
unassigned_bucket = bucket
continue
if (
settings.node_region == "overseas"
and settings.node_role == "control"
and bucket["node_code"] == str(settings.node_code or "").strip()
):
continue
assigned_buckets.append(bucket)
assigned_total += bucket["items_total"]
if unassigned_bucket and unassigned_bucket["items_total"] > 0:
assigned_buckets.append(unassigned_bucket)
assigned_total += unassigned_bucket["items_total"]
effective_total = max(normalized_total, assigned_total)
remainder_total = max(effective_total - assigned_total, 0)
if remainder_total > 0:
if unassigned_bucket:
unassigned_bucket["items_total"] += remainder_total
unassigned_bucket["items_pending"] += remainder_total
assigned_buckets = [
unassigned_bucket if str(item.get("node_code") or "").strip() == "unassigned" else item
for item in assigned_buckets
]
else:
assigned_buckets.append(
{
"node_code": "unassigned",
"items_total": remainder_total,
"items_pending": remainder_total,
"items_claimed": 0,
"items_running": 0,
"display_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
"metrics_source": "central_queue",
"region": "",
"role": "",
"status": "",
"current_load": 0,
"active_threads": 0,
"max_threads": 0,
"last_heartbeat_at": "",
}
)
return sorted(
assigned_buckets,
key=lambda item: (
str(item.get("node_code") or "") == "unassigned",
-_int_value(item.get("active_threads")),
-_int_value(item.get("items_running")),
-_int_value(item.get("items_claimed")),
-_int_value(item.get("items_total")),
str(item.get("node_code") or ""),
),
)
def _build_effective_summary(
*,
node_stats: list[dict],
raw_items_total: int,
raw_items_pending: int,
raw_items_claimed: int,
raw_items_running: int,
raw_items_completed: int,
raw_items_blacklisted: int,
raw_items_failed: int,
) -> dict:
effective_total = max(0, _int_value(raw_items_total))
effective_pending = max(0, _int_value(raw_items_pending))
effective_claimed = max(0, _int_value(raw_items_claimed))
effective_running = max(0, _int_value(raw_items_running))
effective_completed = max(0, _int_value(raw_items_completed))
effective_blacklisted = max(0, _int_value(raw_items_blacklisted))
effective_failed = max(0, _int_value(raw_items_failed))
effective_terminal = effective_completed + effective_blacklisted + effective_failed
return {
"items_total": effective_total,
"items_pending": effective_pending,
"items_claimed": effective_claimed,
"items_running": effective_running,
"items_completed": effective_completed,
"items_blacklisted": effective_blacklisted,
"items_failed": effective_failed,
"items_terminal": max(effective_terminal, 0),
}
def _build_step_bucket(step_code: str) -> dict:
normalized_step_code = str(step_code or "").strip()
return {
"step_code": normalized_step_code,
"step_name": _step_label(normalized_step_code),
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"started_recent": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
}
def _extract_current_cycle_events(events: list[dict]) -> tuple[str, list[dict]]:
if not events:
return "", []
latest_payload = events[0].get("payload") or {}
latest_cycle_token = str(latest_payload.get("cycle_token") or "").strip()
if latest_cycle_token:
current_cycle = []
for event in events:
payload = event.get("payload") or {}
event_cycle = str(payload.get("cycle_token") or "").strip()
if event_cycle == latest_cycle_token:
current_cycle.append(event)
return latest_cycle_token, current_cycle
cycle_token = ""
anchor_index = -1
for index, event in enumerate(events):
payload = event.get("payload") or {}
event_type = str(event.get("event_type") or "")
candidate = str(payload.get("cycle_token") or "").strip()
if candidate and event_type in {"job_dispatch_sent", "job_dispatch_requested", "job_dispatch_failed", "job_dispatch_rejected"}:
cycle_token = candidate
anchor_index = index
break
if not cycle_token:
return "", events
current_cycle = []
for index, event in enumerate(events[: anchor_index + 1]):
payload = event.get("payload") or {}
event_cycle = str(payload.get("cycle_token") or "").strip()
if index == anchor_index or event_cycle == cycle_token:
current_cycle.append(event)
return cycle_token, current_cycle
def _classify_runtime_debug_event(*, event_type: str, message: str, payload: dict) -> dict:
normalized_event_type = str(event_type or "").strip()
normalized_message = str(message or "").strip()
normalized_payload = payload if isinstance(payload, dict) else {}
normalized_job_code = str(normalized_payload.get("job_code") or "").strip()
normalized_node_code = str(normalized_payload.get("node_code") or "").strip()
result = {
"job_code": normalized_job_code,
"node_code": normalized_node_code,
"started": False,
"terminal": False,
"terminal_status": "",
"step_code": "",
}
if normalized_event_type in {"domain_started", "domain_completed", "domain_failed", "domain_blacklisted"}:
result["started"] = normalized_event_type == "domain_started"
result["terminal"] = normalized_event_type in {"domain_completed", "domain_failed", "domain_blacklisted"}
terminal_map = {
"domain_completed": "completed",
"domain_failed": "failed",
"domain_blacklisted": "blacklisted",
}
result["terminal_status"] = terminal_map.get(normalized_event_type, "")
result["step_code"] = normalize_detect_step_code(normalized_payload.get("detect_key")) or "detect_register"
return result
if normalized_event_type != "worker_log":
return result
step_markers = (
("detect_register", ("| step=注册状态检测 |", "detect_register")),
("detect_baidu_site", ("| step=百度site检测 |", "detect_baidu_site", "百度阶段", "detect.baidu")),
("detect_360_site", ("| step=360收录检测 |", "detect_360_site", "360阶段", "detect.c360")),
("detect_chinaz", ("| step=站长之家检测 |", "detect_chinaz", "站长之家", "detect.chinaz")),
("detect_aizhan", ("| step=爱站检测 |", "detect_aizhan", "爱站", "detect.aizhan")),
("detect_wayback", ("| step=时光机检测 |", "detect_wayback", "时光机检测")),
("detect_jucha", ("| step=聚查检测 |", "detect_jucha", "聚查", "detect.jucha")),
("detect_juziseo", ("| step=桔子检测 |", "detect_juziseo", "桔子", "juziseo")),
)
for step_code, markers in step_markers:
if any(marker in normalized_message for marker in markers):
result["step_code"] = step_code
break
if "| stage=started |" in normalized_message or "开始检测域名:" in normalized_message:
result["started"] = True
return result
if "| stage=single_step_finalized |" in normalized_message:
result["terminal"] = True
if "final_status=completed" in normalized_message:
result["terminal_status"] = "completed"
elif "final_status=failed" in normalized_message:
result["terminal_status"] = "failed"
elif "final_status=blacklisted" in normalized_message:
result["terminal_status"] = "blacklisted"
return result
return result
def _load_runtime_activity_snapshot(window_minutes: int) -> dict:
safe_window_minutes = max(5, min(int(window_minutes or 15), 120))
snapshot = {
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
"focus_job_code": "",
"job_codes": [],
"step_code": "detect_register",
"step_stats": {},
"nodes": {},
}
if not (settings.node_region == "overseas" and settings.node_role == "control"):
return snapshot
if settings.node_region == "overseas" and settings.node_role == "control":
source_region = "mainland"
else:
source_region = _normalize_sync_region(settings.sync_source_region, settings.node_region)
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
COALESCE(NULLIF(node_code, ''), 'unknown') AS node_code,
event_type,
message,
payload_json,
created_at
FROM detect_debug_events
WHERE source_region = %s
AND created_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
AND event_type IN ('worker_log', 'domain_started', 'domain_completed', 'domain_failed', 'domain_blacklisted')
ORDER BY created_at DESC, id DESC
LIMIT 2000
""",
(source_region, safe_window_minutes),
)
rows = cur.fetchall()
snapshot_job_codes = _load_recent_runtime_snapshot_job_codes(safe_window_minutes)
job_activity: dict[str, dict] = {}
overall_totals = {
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
}
overall_node_stats: dict[str, dict] = {}
overall_step_activity: dict[str, dict] = {}
per_job_totals: dict[str, dict] = {}
per_job_node_stats: dict[str, dict[str, dict]] = {}
per_job_step_activity: dict[str, dict[str, dict]] = {}
for row in rows:
node_code = str(row[0] or "unknown").strip() or "unknown"
event_type = str(row[1] or "").strip()
message = str(row[2] or "").strip()
payload = _decode_payload(row[3])
created_at = _format_time(row[4])
classified = _classify_runtime_debug_event(event_type=event_type, message=message, payload=payload)
job_code = str(classified.get("job_code") or "").strip()
step_code = str(classified.get("step_code") or "").strip() or "detect_register"
if job_code:
bucket = job_activity.setdefault(
job_code,
{
"job_code": job_code,
"activity_count": 0,
"started_count": 0,
"terminal_count": 0,
"latest_at": "",
},
)
bucket["activity_count"] += 1
if classified.get("started"):
bucket["started_count"] += 1
if classified.get("terminal"):
bucket["terminal_count"] += 1
if created_at > str(bucket.get("latest_at") or ""):
bucket["latest_at"] = created_at
if classified.get("terminal"):
overall_totals["processed_recent"] += 1
step_bucket = overall_step_activity.setdefault(
step_code,
{
"step_code": step_code,
"started_recent": 0,
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
},
)
step_bucket["processed_recent"] += 1
node_bucket = overall_node_stats.setdefault(
node_code,
{
"node_code": node_code,
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
},
)
node_bucket["processed_recent"] += 1
if job_code:
job_totals = per_job_totals.setdefault(
job_code,
{
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
},
)
job_totals["processed_recent"] += 1
job_step_bucket = per_job_step_activity.setdefault(job_code, {}).setdefault(
step_code,
{
"step_code": step_code,
"started_recent": 0,
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
},
)
job_step_bucket["processed_recent"] += 1
job_node_bucket = per_job_node_stats.setdefault(job_code, {}).setdefault(
node_code,
{
"node_code": node_code,
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
},
)
job_node_bucket["processed_recent"] += 1
terminal_status = str(classified.get("terminal_status") or "").strip()
if terminal_status == "completed":
overall_totals["completed_recent"] += 1
node_bucket["completed_recent"] += 1
step_bucket["completed_recent"] += 1
if job_code:
job_totals["completed_recent"] += 1
job_node_bucket["completed_recent"] += 1
job_step_bucket["completed_recent"] += 1
elif terminal_status == "failed":
overall_totals["failed_recent"] += 1
node_bucket["failed_recent"] += 1
step_bucket["failed_recent"] += 1
if job_code:
job_totals["failed_recent"] += 1
job_node_bucket["failed_recent"] += 1
job_step_bucket["failed_recent"] += 1
elif terminal_status == "blacklisted":
overall_totals["blacklisted_recent"] += 1
node_bucket["blacklisted_recent"] += 1
step_bucket["blacklisted_recent"] += 1
if job_code:
job_totals["blacklisted_recent"] += 1
job_node_bucket["blacklisted_recent"] += 1
job_step_bucket["blacklisted_recent"] += 1
if classified.get("started"):
step_bucket = overall_step_activity.setdefault(
step_code,
{
"step_code": step_code,
"started_recent": 0,
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
},
)
step_bucket["started_recent"] += 1
if job_code:
job_step_bucket = per_job_step_activity.setdefault(job_code, {}).setdefault(
step_code,
{
"step_code": step_code,
"started_recent": 0,
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
},
)
job_step_bucket["started_recent"] += 1
ordered_jobs = []
if job_activity:
ordered_jobs = sorted(
job_activity.values(),
key=lambda item: (
-int(item.get("terminal_count", 0) or 0),
-int(item.get("activity_count", 0) or 0),
str(item.get("latest_at") or ""),
str(item.get("job_code") or ""),
),
)
ordered_activity_codes = [
str(item.get("job_code") or "").strip()
for item in ordered_jobs
if str(item.get("job_code") or "").strip()
]
merged_job_codes: list[str] = []
seen_job_codes: set[str] = set()
for job_code in [*snapshot_job_codes, *ordered_activity_codes]:
normalized_job_code = str(job_code or "").strip()
if not normalized_job_code or normalized_job_code in seen_job_codes:
continue
merged_job_codes.append(normalized_job_code)
seen_job_codes.add(normalized_job_code)
focus_job_code = merged_job_codes[0] if merged_job_codes else ""
if focus_job_code:
snapshot["focus_job_code"] = focus_job_code
snapshot["job_codes"] = merged_job_codes[:5]
elif ordered_activity_codes:
snapshot["focus_job_code"] = ordered_activity_codes[0]
snapshot["job_codes"] = ordered_activity_codes[:5]
selected_totals = overall_totals
selected_step_activity = overall_step_activity
selected_node_stats = overall_node_stats
if focus_job_code:
if focus_job_code in per_job_totals:
selected_totals = dict(per_job_totals.get(focus_job_code) or {})
if focus_job_code in per_job_step_activity:
selected_step_activity = dict(per_job_step_activity.get(focus_job_code) or {})
if focus_job_code in per_job_node_stats:
selected_node_stats = dict(per_job_node_stats.get(focus_job_code) or {})
snapshot["processed_recent"] = int(selected_totals.get("processed_recent", 0) or 0)
snapshot["completed_recent"] = int(selected_totals.get("completed_recent", 0) or 0)
snapshot["failed_recent"] = int(selected_totals.get("failed_recent", 0) or 0)
snapshot["blacklisted_recent"] = int(selected_totals.get("blacklisted_recent", 0) or 0)
if selected_step_activity:
ordered_steps = sorted(
selected_step_activity.values(),
key=lambda item: (
-int(item.get("processed_recent", 0) or 0),
-int(item.get("started_recent", 0) or 0),
str(item.get("step_code") or ""),
),
)
snapshot["step_code"] = str((ordered_steps[0] or {}).get("step_code") or "detect_register")
snapshot["step_stats"] = {
str(item.get("step_code") or ""): {
"step_code": str(item.get("step_code") or ""),
"started_recent": int(item.get("started_recent", 0) or 0),
"processed_recent": int(item.get("processed_recent", 0) or 0),
"completed_recent": int(item.get("completed_recent", 0) or 0),
"failed_recent": int(item.get("failed_recent", 0) or 0),
"blacklisted_recent": int(item.get("blacklisted_recent", 0) or 0),
}
for item in ordered_steps
if str(item.get("step_code") or "").strip()
}
snapshot["nodes"] = selected_node_stats
return snapshot
def _load_recent_runtime_snapshot_job_codes(window_minutes: int, *, limit: int = 5) -> list[str]:
safe_window_minutes = max(5, min(int(window_minutes or 15), 12 * 60))
safe_limit = max(1, min(int(limit or 5), 10))
if not (settings.node_region == "overseas" and settings.node_role == "control"):
return []
entries: list[tuple[datetime, str]] = []
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT payload_json, created_at
FROM detect_debug_events
WHERE source_region = 'mainland'
AND service = 'detect-runtime'
AND event_type = 'active_job_snapshot'
AND created_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
ORDER BY created_at DESC, id DESC
LIMIT 12
""",
(safe_window_minutes,),
)
for payload_json, created_at in list(cur.fetchall() or []):
payload = _decode_payload(payload_json)
job_code = str((payload.get("job") or {}).get("job_code") or "").strip() if isinstance(payload, dict) else ""
if job_code and created_at:
entries.append((created_at, job_code))
cur.execute(
"""
SELECT payload_json, COALESCE(updated_at, created_at)
FROM detect_sync_records
WHERE sync_type = 'runtime_ingest'
AND source_region = 'mainland'
AND target_region = 'overseas'
AND created_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
ORDER BY COALESCE(updated_at, created_at) DESC, id DESC
LIMIT 12
""",
(safe_window_minutes,),
)
for payload_json, created_at in list(cur.fetchall() or []):
payload = _decode_payload(payload_json)
projection = payload.get("projection") if isinstance(payload, dict) else {}
job_code = str((projection.get("active_job") or {}).get("job_code") or "").strip() if isinstance(projection, dict) else ""
if job_code and created_at:
entries.append((created_at, job_code))
codes: list[str] = []
seen_codes: set[str] = set()
for _created_at, job_code in sorted(entries, key=lambda item: item[0], reverse=True):
if job_code in seen_codes:
continue
codes.append(job_code)
seen_codes.add(job_code)
if len(codes) >= safe_limit:
break
return codes
def _load_latest_runtime_debug_active_job_snapshot(
window_minutes: int,
*,
preferred_job_codes: list[str] | None = None,
) -> dict:
safe_window_minutes = max(5, min(int(window_minutes or 15), 120))
if not (settings.node_region == "overseas" and settings.node_role == "control"):
return {}
preferred = {
str(item or "").strip()
for item in list(preferred_job_codes or [])
if str(item or "").strip()
}
source_region = "mainland"
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT payload_json, created_at
FROM detect_debug_events
WHERE source_region = %s
AND service = 'detect-runtime'
AND event_type = 'active_job_snapshot'
AND created_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
ORDER BY created_at DESC, id DESC
LIMIT 12
""",
(source_region, safe_window_minutes),
)
rows = list(cur.fetchall() or [])
fallback_snapshot: dict = {}
for row in rows:
payload = _decode_payload(row[0])
if not isinstance(payload, dict):
continue
snapshot = dict(payload)
snapshot["_created_at"] = _format_time(row[1])
snapshot["_snapshot_source"] = "active_job_snapshot"
if not fallback_snapshot:
fallback_snapshot = snapshot
job_code = str((snapshot.get("job") or {}).get("job_code") or "").strip()
if preferred and job_code in preferred:
return snapshot
return fallback_snapshot
def _normalize_runtime_projection_nodes(
*,
active_job: dict,
progress: dict,
projection: dict,
) -> list[dict]:
node_info = dict(projection.get("node") or {})
fallback_node_code = str(node_info.get("node_code") or "").strip()
fallback_region = str(node_info.get("region") or "").strip()
fallback_role = str(node_info.get("role") or "").strip()
active_thread_count = _int_value(projection.get("active_thread_count"))
max_thread_count = _int_value(projection.get("max_thread_count"))
nodes: list[dict] = []
for raw_item in list(active_job.get("distributed_node_stats") or active_job.get("node_stats") or []):
if not isinstance(raw_item, dict):
continue
node_code = str(raw_item.get("node_code") or "").strip()
if not node_code:
continue
nodes.append(
_normalize_node_bucket(
{
**raw_item,
"node_code": node_code,
"metrics_source": str(raw_item.get("metrics_source") or "runtime_ingest").strip() or "runtime_ingest",
"region": str(raw_item.get("region") or (fallback_region if node_code == fallback_node_code else "")).strip(),
"role": str(raw_item.get("role") or (fallback_role if node_code == fallback_node_code else "")).strip(),
"status": str(raw_item.get("status") or "").strip(),
}
)
)
if fallback_node_code:
matched = False
for item in nodes:
if str(item.get("node_code") or "").strip() != fallback_node_code:
continue
matched = True
item["region"] = str(item.get("region") or fallback_region).strip()
item["role"] = str(item.get("role") or fallback_role).strip()
item["active_threads"] = max(_int_value(item.get("active_threads")), active_thread_count)
item["max_threads"] = max(_int_value(item.get("max_threads")), max_thread_count)
item["current_load"] = _effective_runtime_load(
items_running=item.get("items_running"),
active_threads=item.get("active_threads"),
)
item["display_running"] = item["current_load"]
if not str(item.get("status") or "").strip():
item["status"] = "busy" if item["current_load"] > 0 else "online"
break
has_child_instances = any(
str(item.get("node_code") or "").strip().startswith(f"{fallback_node_code}-")
for item in nodes
)
if not matched and not has_child_instances and (
int(active_job.get("items_total", 0) or 0) > 0
or active_thread_count > 0
or max_thread_count > 0
):
nodes.append(
_normalize_node_bucket(
{
"node_code": fallback_node_code,
"items_total": int(active_job.get("items_total", 0) or 0),
"items_pending": int(progress.get("pending", 0) or 0),
"items_claimed": max(
0,
int(active_job.get("items_total", 0) or 0)
- int(progress.get("pending", 0) or 0)
- int(progress.get("running", 0) or 0)
- int(active_job.get("items_terminal", 0) or 0),
),
"items_running": int(progress.get("running", 0) or 0),
"items_completed": int(progress.get("completed", 0) or 0),
"items_blacklisted": int(progress.get("blacklisted", 0) or 0),
"items_failed": max(
int(progress.get("failed", 0) or 0),
int(active_job.get("items_failed", 0) or 0),
),
"active_threads": active_thread_count,
"max_threads": max_thread_count,
"metrics_source": "runtime_ingest",
"region": fallback_region,
"role": fallback_role,
"status": "busy" if max(active_thread_count, int(progress.get("running", 0) or 0)) > 0 else "online",
}
)
)
items_total = max(0, int(active_job.get("items_total", 0) or 0))
pending = max(0, int(progress.get("pending", 0) or 0))
running = max(0, int(progress.get("running", 0) or 0))
completed = max(0, int(progress.get("completed", 0) or 0))
blacklisted = max(0, int(progress.get("blacklisted", 0) or 0))
failed = max(
max(0, int(progress.get("failed", 0) or 0)),
max(0, int(active_job.get("items_failed", 0) or 0)),
)
terminal = max(
max(0, int(active_job.get("items_terminal", 0) or 0)),
completed + blacklisted + failed,
)
claimed = max(0, items_total - pending - running - terminal)
assigned_totals = {
"items_total": sum(_int_value(item.get("items_total")) for item in nodes),
"items_pending": sum(_int_value(item.get("items_pending")) for item in nodes),
"items_claimed": sum(_int_value(item.get("items_claimed")) for item in nodes),
"items_running": sum(_int_value(item.get("items_running")) for item in nodes),
"items_completed": sum(_int_value(item.get("items_completed")) for item in nodes),
"items_blacklisted": sum(_int_value(item.get("items_blacklisted")) for item in nodes),
"items_failed": sum(_int_value(item.get("items_failed")) for item in nodes),
}
unassigned_bucket = {
"node_code": "unassigned",
"items_total": max(0, items_total - assigned_totals["items_total"]),
"items_pending": max(0, pending - assigned_totals["items_pending"]),
"items_claimed": max(0, claimed - assigned_totals["items_claimed"]),
"items_running": max(0, running - assigned_totals["items_running"]),
"items_completed": max(0, completed - assigned_totals["items_completed"]),
"items_blacklisted": max(0, blacklisted - assigned_totals["items_blacklisted"]),
"items_failed": max(0, failed - assigned_totals["items_failed"]),
"metrics_source": "runtime_ingest",
"status": "online",
}
if any(int(unassigned_bucket.get(key, 0) or 0) > 0 for key in (
"items_total",
"items_pending",
"items_claimed",
"items_running",
"items_completed",
"items_blacklisted",
"items_failed",
)):
nodes.append(_normalize_node_bucket(unassigned_bucket))
return nodes
def _build_runtime_snapshot_from_projection(
projection: dict,
*,
created_at: datetime | None = None,
window_minutes: int = 15,
) -> dict:
normalized_projection = dict(projection or {})
active_job = dict(normalized_projection.get("active_job") or {})
progress = dict(normalized_projection.get("progress") or {})
backlog = dict(normalized_projection.get("backlog") or {})
nodes = _normalize_runtime_projection_nodes(
active_job=active_job,
progress=progress,
projection=normalized_projection,
)
items_total = max(0, int(active_job.get("items_total", 0) or 0))
pending = max(0, int(progress.get("pending", active_job.get("items_pending", 0)) or 0))
running = max(0, int(progress.get("running", active_job.get("items_running", 0)) or 0))
completed = max(0, int(progress.get("completed", 0) or 0))
blacklisted = max(0, int(progress.get("blacklisted", 0) or 0))
failed = max(
max(0, int(progress.get("failed", 0) or 0)),
max(0, int(active_job.get("items_failed", 0) or 0)),
)
terminal = max(
max(0, int(active_job.get("items_terminal", 0) or 0)),
completed + blacklisted + failed,
)
if items_total <= 0:
items_total = pending + running + terminal
claimed = max(0, items_total - pending - running - terminal)
display_running = _max_runtime_metric(
running,
normalized_projection.get("active_thread_count"),
active_job.get("display_active_threads"),
active_job.get("display_items_running"),
)
display_claimed = _max_runtime_metric(claimed, active_job.get("display_items_claimed"))
progress_percent = float(active_job.get("progress_percent", 0) or 0)
if progress_percent <= 0 and items_total > 0 and terminal > 0:
progress_percent = round((terminal / items_total) * 100, 2)
queue_health = {
"window_minutes": max(5, min(int(window_minutes or 15), 120)),
"has_active_job": bool(str(active_job.get("job_code") or "").strip() or items_total > 0),
"job": {
"job_id": int(active_job.get("job_id", 0) or 0),
"job_code": str(active_job.get("job_code") or "").strip(),
"runtime_job_code": str(active_job.get("job_code") or "").strip(),
"status": str(active_job.get("status") or "").strip(),
"progress_percent": progress_percent,
},
"queue": {
"items_total": items_total,
"pending": pending,
"claimed": claimed,
"running": running,
"display_claimed": display_claimed,
"display_running": display_running,
"completed": completed,
"blacklisted": blacklisted,
"failed": failed,
"terminal": terminal,
"terminal_percent": round((terminal / items_total) * 100, 2) if items_total else 0.0,
"oldest_pending_at": "",
"oldest_pending_age_minutes": 0,
"nearest_lease_expiry_at": "",
"overdue_leases": 0,
"expiring_soon_leases": 0,
},
"throughput": {
"processed_recent": 0,
"processed_per_minute": 0.0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
},
"nodes": nodes,
"steps": [],
"runtime_activity": {},
}
snapshot = {
"job": {
"job_id": int(active_job.get("job_id", 0) or 0),
"job_code": str(active_job.get("job_code") or "").strip(),
"status": str(active_job.get("status") or "").strip(),
"items_total": items_total,
"items_pending": pending,
"items_claimed": claimed,
"items_running": running,
"items_completed": completed,
"items_blacklisted": blacklisted,
"items_failed": failed,
"items_terminal": terminal,
"progress_percent": progress_percent,
"node_stats": nodes,
},
"queue_health": queue_health,
"backlog": backlog if isinstance(backlog, dict) else {},
"recent_events": [],
"_snapshot_source": "runtime_ingest",
}
if created_at:
snapshot["_created_at"] = _format_time(created_at)
return snapshot
def _snapshot_created_at(snapshot: dict) -> datetime | None:
return _parse_runtime_timestamp(snapshot.get("_created_at"))
def _parse_runtime_timestamp(value: object) -> datetime | None:
raw_value = str(value or "").strip()
if not raw_value:
return None
normalized_value = raw_value.replace("T", " ")
if normalized_value.endswith("Z"):
normalized_value = f"{normalized_value[:-1]}+00:00"
try:
return datetime.fromisoformat(normalized_value)
except ValueError:
for fmt in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S"):
try:
return datetime.strptime(normalized_value, fmt)
except ValueError:
continue
return None
def _runtime_timestamp_is_fresh(value: datetime | None, *, max_age: timedelta) -> bool:
if value is None:
return False
now = datetime.now(value.tzinfo) if value.tzinfo else datetime.now()
return now - value <= max_age
def _runtime_node_display_running(item: dict) -> int:
return _max_runtime_metric(
item.get("display_running"),
item.get("current_load"),
item.get("active_threads"),
item.get("items_running"),
)
def _load_runtime_node_overlay_map(node_codes: list[str]) -> dict[str, dict]:
normalized_codes = [
str(item or "").strip()
for item in list(node_codes or [])
if str(item or "").strip() and str(item or "").strip() != "unassigned"
]
if not normalized_codes:
return {}
if not (settings.node_region == "overseas" and settings.node_role == "control"):
return {}
disabled_node_codes = _load_disabled_managed_node_codes(normalized_codes)
try:
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT node_code, region, role, status, current_load, metadata_json, last_heartbeat_at
FROM detect_worker_nodes
WHERE node_code = ANY(%s)
""",
(normalized_codes,),
)
rows = list(cur.fetchall() or [])
except Exception:
return {}
overlays: dict[str, dict] = {}
for row in rows:
node_code = str((row or [""])[0] or "").strip()
if not node_code or node_code in disabled_node_codes:
continue
metadata = _decode_payload((row or ["", "", "", "", 0, {}])[5])
overlays[node_code] = {
"region": str((row or ["", "", ""])[1] or "").strip(),
"role": str((row or ["", "", "", ""])[2] or "").strip(),
"status": str((row or ["", "", "", "", ""])[3] or "").strip(),
"current_load": _int_value((row or ["", "", "", "", 0])[4]),
"active_threads": _int_value(metadata.get("active_threads")),
"max_threads": _int_value(metadata.get("max_threads")),
"last_heartbeat_at": _format_time((row or ["", "", "", "", 0, {}, None])[6]) if len(row or []) > 6 and row[6] else "",
"metrics_source": str(metadata.get("service") or "runtime").strip() or "runtime",
}
return overlays
def _merge_runtime_node_overlay(item: dict, overlay: dict | None) -> dict:
merged = dict(item or {})
normalized_overlay = dict(overlay or {})
if not normalized_overlay:
return merged
for key in ("region", "role", "status", "last_heartbeat_at", "metrics_source"):
if not str(merged.get(key) or "").strip() and str(normalized_overlay.get(key) or "").strip():
merged[key] = normalized_overlay.get(key)
for key in ("current_load", "active_threads", "max_threads"):
merged[key] = max(_int_value(merged.get(key)), _int_value(normalized_overlay.get(key)))
return merged
def _filter_live_runtime_nodes(nodes: list[dict], *, snapshot_created_at: datetime | None) -> list[dict]:
filtered: list[dict] = []
for raw_item in list(nodes or []):
if not isinstance(raw_item, dict):
continue
item = dict(raw_item)
node_code = str(item.get("node_code") or "").strip()
if not node_code:
continue
if node_code == "unassigned":
filtered.append(item)
continue
status = str(item.get("status") or "").strip().lower()
if status in {"stale", "offline"}:
continue
reference_at = _parse_runtime_timestamp(item.get("last_heartbeat_at"))
if reference_at is None:
filtered.append(item)
continue
if not _runtime_timestamp_is_fresh(reference_at, max_age=_LIVE_RUNTIME_NODE_STALE_AFTER):
continue
filtered.append(item)
return filtered
def _build_live_runtime_snapshot(snapshot: dict | None) -> dict:
normalized_snapshot = dict(snapshot or {})
if not normalized_snapshot:
return {}
snapshot_created_at = _snapshot_created_at(normalized_snapshot)
job = dict(normalized_snapshot.get("job") or {})
queue_health = dict(normalized_snapshot.get("queue_health") or {})
queue = dict(queue_health.get("queue") or {})
queue_nodes = [dict(item) for item in list(queue_health.get("nodes") or []) if isinstance(item, dict)]
job_nodes = [dict(item) for item in list(job.get("node_stats") or []) if isinstance(item, dict)]
overlay_map = {}
if str(normalized_snapshot.get("_snapshot_source") or "").strip() == "runtime_ingest":
overlay_map = _load_runtime_node_overlay_map(
[
*(item.get("node_code") for item in queue_nodes),
*(item.get("node_code") for item in job_nodes),
]
)
queue_nodes = [
_merge_runtime_node_overlay(item, overlay_map.get(str(item.get("node_code") or "").strip()))
for item in queue_nodes
]
job_nodes = [
_merge_runtime_node_overlay(item, overlay_map.get(str(item.get("node_code") or "").strip()))
for item in job_nodes
]
live_queue_nodes = _filter_live_runtime_nodes(queue_nodes, snapshot_created_at=snapshot_created_at)
live_job_nodes = _filter_live_runtime_nodes(job_nodes, snapshot_created_at=snapshot_created_at)
has_runtime_node_rows = bool(queue_nodes or job_nodes)
raw_node_count = len([item for item in queue_nodes or job_nodes if str(item.get("node_code") or "").strip() and str(item.get("node_code") or "").strip() != "unassigned"])
live_nodes_for_display = live_queue_nodes if queue_nodes else live_job_nodes
live_participant_nodes = [
item
for item in live_nodes_for_display
if str(item.get("node_code") or "").strip()
and str(item.get("node_code") or "").strip() != "unassigned"
]
dropped_node_count = max(0, raw_node_count - len(live_participant_nodes))
display_running = sum(_runtime_node_display_running(item) for item in live_participant_nodes)
display_claimed = sum(
_max_runtime_metric(item.get("display_claimed"), item.get("items_claimed"))
for item in live_participant_nodes
)
display_max_threads = sum(max(0, _int_value(item.get("max_threads"))) for item in live_participant_nodes)
allow_raw_display_fallback = dropped_node_count <= 0 and (
not has_runtime_node_rows or bool(live_participant_nodes)
)
if allow_raw_display_fallback:
display_running = _max_runtime_metric(
display_running,
queue.get("display_running"),
job.get("display_items_running"),
job.get("display_active_threads"),
normalized_snapshot.get("active_thread_count"),
job.get("items_running"),
queue.get("running"),
)
display_claimed = _max_runtime_metric(
display_claimed,
queue.get("display_claimed"),
job.get("display_items_claimed"),
queue.get("claimed"),
job.get("items_claimed"),
)
display_max_threads = _max_runtime_metric(
display_max_threads,
job.get("display_max_threads"),
normalized_snapshot.get("max_thread_count"),
)
if queue_nodes or job_nodes:
queue_health["nodes"] = list(live_queue_nodes or live_job_nodes)
queue["display_running"] = display_running
queue["display_claimed"] = display_claimed
queue_health["queue"] = queue
job["node_stats"] = list(live_job_nodes or live_queue_nodes)
job["display_items_running"] = display_running
job["display_active_threads"] = display_running
job["display_max_threads"] = display_max_threads
normalized_snapshot["job"] = job
normalized_snapshot["queue_health"] = queue_health
normalized_snapshot["_raw_runtime_node_count"] = raw_node_count
normalized_snapshot["_live_runtime_node_count"] = len(live_participant_nodes)
normalized_snapshot["_dropped_runtime_node_count"] = dropped_node_count
normalized_snapshot["_has_runtime_node_rows"] = has_runtime_node_rows
return normalized_snapshot
def _snapshot_display_running(snapshot: dict) -> int:
queue = dict((snapshot.get("queue_health") or {}).get("queue") or {})
job = dict(snapshot.get("job") or {})
return max(
_int_value(queue.get("display_running")),
_int_value(job.get("display_items_running")),
_int_value(job.get("display_active_threads")),
_int_value(snapshot.get("active_thread_count")),
)
def _runtime_job_matches_active_job(
active_job: dict | None,
*,
runtime_job_code: object = "",
runtime_job_id: object = 0,
) -> bool:
normalized_active_job = dict(active_job or {})
active_job_code = str(normalized_active_job.get("job_code") or "").strip()
active_job_id = _int_value(normalized_active_job.get("job_id"))
normalized_runtime_job_code = str(runtime_job_code or "").strip()
normalized_runtime_job_id = _int_value(runtime_job_id)
if active_job_id > 0 and normalized_runtime_job_id > 0 and active_job_id == normalized_runtime_job_id:
return True
if active_job_code and normalized_runtime_job_code and active_job_code == normalized_runtime_job_code:
return True
return False
def _snapshot_node_count(snapshot: dict) -> int:
queue_nodes = list((snapshot.get("queue_health") or {}).get("nodes") or [])
if queue_nodes:
return len(
[
item
for item in queue_nodes
if isinstance(item, dict) and str(item.get("node_code") or "").strip() != "unassigned"
]
)
job_nodes = list((snapshot.get("job") or {}).get("node_stats") or [])
return len(
[
item
for item in job_nodes
if isinstance(item, dict) and str(item.get("node_code") or "").strip() != "unassigned"
]
)
def _snapshot_is_stale_for_live_display(snapshot: dict) -> bool:
normalized_snapshot = dict(snapshot or {})
if str(normalized_snapshot.get("_snapshot_source") or "").strip() != "runtime_ingest":
return False
created_at = _snapshot_created_at(normalized_snapshot)
if created_at is None:
return False
return not _runtime_timestamp_is_fresh(created_at, max_age=_PREFERRED_RUNTIME_SNAPSHOT_GRACE)
def _should_prefer_fallback_runtime_snapshot(*, fallback_snapshot: dict, matched_snapshot: dict) -> bool:
fallback_nodes = _snapshot_node_count(fallback_snapshot)
matched_nodes = _snapshot_node_count(matched_snapshot)
fallback_display_running = _snapshot_display_running(fallback_snapshot)
matched_display_running = _snapshot_display_running(matched_snapshot)
if fallback_nodes <= matched_nodes and fallback_display_running <= matched_display_running:
return False
fallback_created_at = _snapshot_created_at(fallback_snapshot)
matched_created_at = _snapshot_created_at(matched_snapshot)
if fallback_created_at and matched_created_at:
return fallback_created_at - matched_created_at > _PREFERRED_RUNTIME_SNAPSHOT_GRACE
return matched_nodes <= 0 and matched_display_running <= 0
def _load_latest_runtime_ingest_active_job_snapshot(
window_minutes: int,
*,
preferred_job_codes: list[str] | None = None,
) -> dict:
safe_window_minutes = max(5, min(int(window_minutes or 15), 120))
if not (settings.node_region == "overseas" and settings.node_role == "control"):
return {}
preferred = {
str(item or "").strip()
for item in list(preferred_job_codes or [])
if str(item or "").strip()
}
query_sql = """
SELECT payload_json, COALESCE(updated_at, created_at)
FROM detect_sync_records
WHERE sync_type = 'runtime_ingest'
AND source_region = 'mainland'
AND target_region = 'overseas'
AND created_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
ORDER BY COALESCE(updated_at, created_at) DESC, id DESC
LIMIT 20
"""
def _select_rows(lookback_minutes: int) -> list[tuple]:
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(query_sql, (int(lookback_minutes),))
return list(cur.fetchall() or [])
def _pick_snapshot(rows: list[tuple]) -> tuple[dict, dict]:
fallback_snapshot: dict = {}
matched_snapshot: dict = {}
for row in rows:
payload = _decode_payload(row[0])
projection = payload.get("projection") if isinstance(payload, dict) else {}
if not isinstance(projection, dict):
continue
snapshot = _build_runtime_snapshot_from_projection(
projection,
created_at=row[1],
window_minutes=safe_window_minutes,
)
if not fallback_snapshot:
fallback_snapshot = snapshot
job_code = str((snapshot.get("job") or {}).get("job_code") or "").strip()
if preferred and job_code in preferred:
matched_snapshot = snapshot
break
return fallback_snapshot, matched_snapshot
recent_rows = _select_rows(safe_window_minutes)
fallback_snapshot, matched_snapshot = _pick_snapshot(recent_rows)
if matched_snapshot:
if (
fallback_snapshot
and fallback_snapshot is not matched_snapshot
and _should_prefer_fallback_runtime_snapshot(
fallback_snapshot=fallback_snapshot,
matched_snapshot=matched_snapshot,
)
):
return fallback_snapshot
return matched_snapshot
if preferred:
extended_rows = _select_rows(max(safe_window_minutes, 12 * 60))
extended_fallback, matched_snapshot = _pick_snapshot(extended_rows)
if matched_snapshot:
if (
extended_fallback
and extended_fallback is not matched_snapshot
and _should_prefer_fallback_runtime_snapshot(
fallback_snapshot=extended_fallback,
matched_snapshot=matched_snapshot,
)
):
return extended_fallback
return matched_snapshot
if not fallback_snapshot:
fallback_snapshot = extended_fallback
return fallback_snapshot
@db_read_retry()
def _load_latest_runtime_active_job_snapshot(window_minutes: int) -> dict:
safe_window_minutes = max(5, min(int(window_minutes or 15), 120))
if not (settings.node_region == "overseas" and settings.node_role == "control"):
return {}
runtime_activity = _load_runtime_activity_snapshot(safe_window_minutes)
focus_job_code = str(runtime_activity.get("focus_job_code") or "").strip()
preferred_job_codes = [focus_job_code] + [
str(item or "").strip()
for item in list(runtime_activity.get("job_codes") or [])
if str(item or "").strip()
]
debug_snapshot = _load_latest_runtime_debug_active_job_snapshot(
safe_window_minutes,
preferred_job_codes=preferred_job_codes,
)
ingest_snapshot = _load_latest_runtime_ingest_active_job_snapshot(
safe_window_minutes,
preferred_job_codes=preferred_job_codes,
)
debug_has_payload = bool(debug_snapshot.get("job")) or bool((debug_snapshot.get("queue_health") or {}).get("queue"))
ingest_has_payload = bool(ingest_snapshot.get("job")) or bool((ingest_snapshot.get("queue_health") or {}).get("queue"))
if ingest_has_payload and not debug_has_payload:
return _build_live_runtime_snapshot(ingest_snapshot)
if debug_has_payload and not ingest_has_payload:
return _build_live_runtime_snapshot(debug_snapshot)
if not debug_has_payload and not ingest_has_payload:
return {}
if focus_job_code:
debug_job_code = str((debug_snapshot.get("job") or {}).get("job_code") or "").strip()
ingest_job_code = str((ingest_snapshot.get("job") or {}).get("job_code") or "").strip()
debug_matches_focus = bool(debug_job_code and debug_job_code == focus_job_code)
ingest_matches_focus = bool(ingest_job_code and ingest_job_code == focus_job_code)
if ingest_matches_focus and not debug_matches_focus:
return _build_live_runtime_snapshot(ingest_snapshot)
if debug_matches_focus and not ingest_matches_focus:
return _build_live_runtime_snapshot(debug_snapshot)
debug_queue = dict((debug_snapshot.get("queue_health") or {}).get("queue") or {})
ingest_queue = dict((ingest_snapshot.get("queue_health") or {}).get("queue") or {})
debug_nodes = list((debug_snapshot.get("queue_health") or {}).get("nodes") or (debug_snapshot.get("job") or {}).get("node_stats") or [])
ingest_nodes = list((ingest_snapshot.get("queue_health") or {}).get("nodes") or (ingest_snapshot.get("job") or {}).get("node_stats") or [])
debug_display_running = max(
_int_value(debug_queue.get("display_running")),
_int_value(((debug_snapshot.get("job") or {}).get("display_items_running"))),
_int_value(((debug_snapshot.get("job") or {}).get("display_active_threads"))),
)
ingest_display_running = max(
_int_value(ingest_queue.get("display_running")),
_int_value(((ingest_snapshot.get("job") or {}).get("display_items_running"))),
_int_value(((ingest_snapshot.get("job") or {}).get("display_active_threads"))),
)
if len(ingest_nodes) > len(debug_nodes) or ingest_display_running > debug_display_running:
return _build_live_runtime_snapshot(ingest_snapshot)
if str(ingest_snapshot.get("_created_at") or "") > str(debug_snapshot.get("_created_at") or ""):
return _build_live_runtime_snapshot(ingest_snapshot)
return _build_live_runtime_snapshot(debug_snapshot)
def _select_runtime_snapshot_events(
runtime_snapshot: dict,
*,
job_code: str = "",
job_id: int = 0,
event_limit: int = 20,
) -> list[dict]:
safe_limit = max(1, min(int(event_limit or 20), 100))
target_job_code = str(job_code or "").strip()
target_job_id = int(job_id or 0)
selected: list[dict] = []
for raw_event in list(runtime_snapshot.get("recent_events") or []):
if not isinstance(raw_event, dict):
continue
payload = raw_event.get("payload")
normalized_payload = payload if isinstance(payload, dict) else _decode_payload(payload)
event_job_code = str(normalized_payload.get("job_code") or "").strip()
event_job_id = _int_value(raw_event.get("job_id"))
if target_job_code:
if event_job_code == target_job_code or (target_job_id > 0 and event_job_id == target_job_id):
selected.append(
{
"node_code": str(raw_event.get("node_code") or "").strip(),
"event_type": str(raw_event.get("event_type") or "").strip(),
"level": str(raw_event.get("level") or "info").strip() or "info",
"message": str(raw_event.get("message") or "").strip(),
"payload": normalized_payload,
"created_at": str(raw_event.get("created_at") or "").strip(),
}
)
else:
selected.append(
{
"node_code": str(raw_event.get("node_code") or "").strip(),
"event_type": str(raw_event.get("event_type") or "").strip(),
"level": str(raw_event.get("level") or "info").strip() or "info",
"message": str(raw_event.get("message") or "").strip(),
"payload": normalized_payload,
"created_at": str(raw_event.get("created_at") or "").strip(),
}
)
if len(selected) >= safe_limit:
break
return selected
def _filter_events_for_job(events: list[dict], *, job_code: str = "", job_id: int = 0, limit: int = 20) -> list[dict]:
target_job_code = str(job_code or "").strip()
target_job_id = int(job_id or 0)
safe_limit = max(1, min(int(limit or 20), 100))
filtered: list[dict] = []
for raw_event in list(events or []):
if not isinstance(raw_event, dict):
continue
payload = raw_event.get("payload")
normalized_payload = payload if isinstance(payload, dict) else _decode_payload(payload)
event_job_code = str(normalized_payload.get("job_code") or "").strip()
event_job_id = _int_value(raw_event.get("job_id"))
if target_job_code:
if event_job_code != target_job_code and (target_job_id <= 0 or event_job_id != target_job_id):
continue
filtered.append(
{
"node_code": str(raw_event.get("node_code") or "").strip(),
"event_type": str(raw_event.get("event_type") or "").strip(),
"level": str(raw_event.get("level") or "info").strip() or "info",
"message": str(raw_event.get("message") or "").strip(),
"payload": normalized_payload,
"created_at": str(raw_event.get("created_at") or "").strip(),
}
)
if len(filtered) >= safe_limit:
break
return filtered
def _build_active_job_summary_from_runtime_snapshot(runtime_snapshot: dict, *, event_limit: int = 20) -> dict | None:
normalized_snapshot = _build_live_runtime_snapshot(runtime_snapshot)
runtime_job = dict(normalized_snapshot.get("job") or {})
runtime_queue = dict((normalized_snapshot.get("queue_health") or {}).get("queue") or {})
if not runtime_job and not runtime_queue:
return None
runtime_nodes = list((normalized_snapshot.get("queue_health") or {}).get("nodes") or runtime_job.get("node_stats") or [])
runtime_steps = list((normalized_snapshot.get("queue_health") or {}).get("steps") or [])
runtime_throughput = dict((normalized_snapshot.get("queue_health") or {}).get("throughput") or {})
normalized_nodes = [
_normalize_node_bucket(item)
for item in runtime_nodes
if isinstance(item, dict) and str(item.get("node_code") or "").strip()
]
display_summary = _build_display_summary(normalized_nodes)
runtime_allow_raw_display_fallback = (
int(normalized_snapshot.get("_dropped_runtime_node_count", 0) or 0) <= 0
and (
not bool(normalized_snapshot.get("_has_runtime_node_rows"))
or bool(display_summary.get("active_nodes") or [])
or any(str(item.get("node_code") or "").strip() != "unassigned" for item in normalized_nodes)
)
)
display_running_candidates = [
runtime_queue.get("display_running"),
display_summary.get("display_running"),
runtime_job.get("display_items_running"),
runtime_job.get("display_active_threads"),
]
if runtime_allow_raw_display_fallback:
display_running_candidates.extend((runtime_job.get("items_running"), runtime_queue.get("running")))
display_items_running = _max_runtime_metric(
*display_running_candidates,
)
display_active_thread_candidates = [
display_summary.get("active_threads"),
runtime_job.get("display_active_threads"),
runtime_job.get("display_items_running"),
runtime_queue.get("display_running"),
]
if runtime_allow_raw_display_fallback:
display_active_thread_candidates.extend((runtime_job.get("items_running"), runtime_queue.get("running")))
display_active_threads = _max_runtime_metric(
*display_active_thread_candidates,
)
display_max_threads = _max_runtime_metric(
display_summary.get("max_threads"),
runtime_job.get("display_max_threads"),
)
items_total = _int_value(runtime_queue.get("items_total", runtime_job.get("items_total", 0)))
items_pending = _int_value(runtime_queue.get("pending", runtime_job.get("items_pending", 0)))
items_claimed = _int_value(runtime_queue.get("claimed", runtime_job.get("items_claimed", 0)))
items_running = _int_value(runtime_queue.get("running", runtime_job.get("items_running", 0)))
items_completed = _int_value(runtime_queue.get("completed", runtime_job.get("items_completed", 0)))
items_blacklisted = _int_value(runtime_queue.get("blacklisted", runtime_job.get("items_blacklisted", 0)))
items_failed = _int_value(runtime_queue.get("failed", runtime_job.get("items_failed", 0)))
items_terminal = _int_value(
runtime_queue.get(
"terminal",
runtime_job.get("items_terminal", items_completed + items_blacklisted + items_failed),
)
)
display_current_load = _max_runtime_metric(
display_summary.get("current_load"),
display_items_running,
display_active_threads,
items_running,
)
recent_events = _select_runtime_snapshot_events(
normalized_snapshot,
job_code=str(runtime_job.get("job_code") or "").strip(),
job_id=int(runtime_job.get("job_id", 0) or 0),
event_limit=event_limit,
)
cycle_token, current_cycle_events = _extract_current_cycle_events(recent_events)
return {
"job_id": int(runtime_job.get("job_id", 0) or 0),
"job_code": str(runtime_job.get("job_code") or "").strip(),
"source": str(normalized_snapshot.get("_snapshot_source") or "runtime").strip() or "runtime",
"task_mode": _DEFAULT_TASK_MODE,
"step_code": "",
"status": str(runtime_job.get("status") or "").strip() or "running",
"created_by": str(normalized_snapshot.get("_snapshot_source") or "runtime").strip() or "runtime",
"created_at": str(normalized_snapshot.get("_created_at") or "").strip(),
"started_at": "",
"finished_at": "",
"items_total": items_total,
"items_pending": items_pending,
"items_claimed": items_claimed,
"items_running": items_running,
"items_completed": items_completed,
"items_blacklisted": items_blacklisted,
"items_failed": items_failed,
"items_terminal": items_terminal,
"progress_percent": float(runtime_job.get("progress_percent", 0) or 0),
"raw_items_total": items_total,
"raw_items_pending": items_pending,
"raw_items_claimed": items_claimed,
"raw_items_running": items_running,
"raw_items_completed": items_completed,
"raw_items_blacklisted": items_blacklisted,
"raw_items_failed": items_failed,
"raw_items_terminal": items_terminal,
"raw_node_stats": list(normalized_nodes),
"raw_step_stats": list(runtime_steps),
"node_stats": list(normalized_nodes),
"distributed_node_stats": list(normalized_nodes),
"step_stats": list(runtime_steps),
"display_items_claimed": _max_runtime_metric(
runtime_queue.get("display_claimed"),
display_summary.get("items_claimed"),
items_claimed,
),
"display_items_running": display_items_running,
"display_current_load": display_current_load,
"display_active_threads": display_active_threads,
"display_max_threads": display_max_threads,
"display_items_completed": int(display_summary.get("items_completed", items_completed) or 0),
"display_items_failed": int(display_summary.get("items_failed", items_failed) or 0),
"display_active_node_codes": list(display_summary.get("active_nodes") or []),
"processed_recent": int(runtime_throughput.get("processed_recent", 0) or 0),
"processed_per_minute": float(runtime_throughput.get("processed_per_minute", 0) or 0),
"completed_recent": int(runtime_throughput.get("completed_recent", 0) or 0),
"blacklisted_recent": int(runtime_throughput.get("blacklisted_recent", 0) or 0),
"failed_recent": int(runtime_throughput.get("failed_recent", 0) or 0),
"recent_events": recent_events,
"latest_event": recent_events[0] if recent_events else None,
"current_cycle_token": cycle_token,
"current_cycle_events": current_cycle_events,
"latest_cycle_event": current_cycle_events[0] if current_cycle_events else (recent_events[0] if recent_events else None),
"recent_domain_events": [],
}
def _fetch_job_summary(cur, job_row, event_limit: int = 20) -> dict:
job_id = job_row[0]
cur.execute(
"""
SELECT status, count(*)
FROM detect_job_items
WHERE job_id = %s
GROUP BY status
""",
(job_id,),
)
counts = {status: int(count) for status, count in cur.fetchall()}
cur.execute(
"""
SELECT COALESCE(NULLIF(claimed_by, ''), 'unassigned') AS node_code,
status,
count(*)
FROM detect_job_items
WHERE job_id = %s
GROUP BY COALESCE(NULLIF(claimed_by, ''), 'unassigned'), status
ORDER BY node_code ASC, status ASC
""",
(job_id,),
)
node_buckets: dict[str, dict] = {}
for node_code, status, count in cur.fetchall():
bucket = node_buckets.setdefault(
node_code,
{
"node_code": node_code,
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
},
)
field_name = f"items_{status}"
if field_name in bucket:
bucket[field_name] += int(count)
bucket["items_total"] += int(count)
cur.execute(
"""
SELECT COALESCE(NULLIF(step_code, ''), 'domain_pipeline') AS step_code, status, count(*)
FROM detect_job_items
WHERE job_id = %s
GROUP BY COALESCE(NULLIF(step_code, ''), 'domain_pipeline'), status
ORDER BY step_code ASC, status ASC
""",
(job_id,),
)
step_buckets: dict[str, dict] = {}
for step_code, status, count in cur.fetchall():
bucket = step_buckets.setdefault(str(step_code or ""), _build_step_bucket(str(step_code or "")))
field_name = f"items_{status}"
if field_name in bucket:
bucket[field_name] += int(count)
bucket["items_total"] += int(count)
cur.execute(
"""
SELECT node_code, event_type, level, message, payload_json, created_at
FROM detect_run_events
WHERE job_id = %s
ORDER BY created_at DESC, id DESC
LIMIT %s
""",
(job_id, max(1, int(event_limit or 20))),
)
events = [
{
"node_code": item[0],
"event_type": item[1],
"level": item[2],
"message": item[3],
"payload": _decode_payload(item[4]),
"created_at": _format_time(item[5]),
}
for item in cur.fetchall()
]
cycle_token, current_cycle_events = _extract_current_cycle_events(events)
distributed_node_stats = _merge_display_node_stats(
local_node_stats=list(node_buckets.values()),
runtime_node_rows=_load_runtime_display_rows(cur),
)
raw_total = sum(counts.values())
raw_pending = int(counts.get("pending", 0))
raw_claimed = int(counts.get("claimed", 0))
raw_running = int(counts.get("running", 0))
raw_completed = int(counts.get("completed", 0))
raw_blacklisted = int(counts.get("blacklisted", 0))
raw_failed = int(counts.get("failed", 0))
raw_terminal = raw_completed + raw_blacklisted + raw_failed
effective_node_stats = _build_effective_node_stats(
distributed_node_stats=distributed_node_stats,
raw_items_total=raw_total,
)
effective_summary = _build_effective_summary(
node_stats=effective_node_stats,
raw_items_total=raw_total,
raw_items_pending=raw_pending,
raw_items_claimed=raw_claimed,
raw_items_running=raw_running,
raw_items_completed=raw_completed,
raw_items_blacklisted=raw_blacklisted,
raw_items_failed=raw_failed,
)
display_summary = _build_display_summary(effective_node_stats)
return {
"job_id": job_id,
"job_code": job_row[1],
"source": job_row[2],
"task_mode": job_row[3] or _DEFAULT_TASK_MODE,
"step_code": job_row[4] or "",
"status": job_row[5],
"created_by": job_row[6],
"created_at": _format_time(job_row[7]),
"started_at": _format_time(job_row[8]),
"finished_at": _format_time(job_row[9]),
"items_total": int(effective_summary.get("items_total", 0) or 0),
"items_pending": int(effective_summary.get("items_pending", 0) or 0),
"items_claimed": int(effective_summary.get("items_claimed", 0) or 0),
"items_running": int(effective_summary.get("items_running", 0) or 0),
"items_completed": int(effective_summary.get("items_completed", 0) or 0),
"items_blacklisted": int(effective_summary.get("items_blacklisted", 0) or 0),
"items_failed": int(effective_summary.get("items_failed", 0) or 0),
"items_terminal": int(effective_summary.get("items_terminal", 0) or 0),
"progress_percent": round(
(int(effective_summary.get("items_terminal", 0) or 0) / int(effective_summary.get("items_total", 0) or 0)) * 100,
2,
) if int(effective_summary.get("items_total", 0) or 0) else 0,
"raw_items_total": raw_total,
"raw_items_pending": raw_pending,
"raw_items_claimed": raw_claimed,
"raw_items_running": raw_running,
"raw_items_completed": raw_completed,
"raw_items_blacklisted": raw_blacklisted,
"raw_items_failed": raw_failed,
"raw_items_terminal": raw_terminal,
"raw_node_stats": list(node_buckets.values()),
"raw_step_stats": list(step_buckets.values()),
"node_stats": effective_node_stats,
"distributed_node_stats": effective_node_stats,
"step_stats": sorted(
step_buckets.values(),
key=lambda item: (
-_int_value(item.get("items_running")),
-_int_value(item.get("items_pending")),
str(item.get("step_code") or ""),
),
),
"display_items_claimed": int(display_summary.get("items_claimed", 0) or 0),
"display_items_running": int(display_summary.get("display_running", display_summary.get("items_running", 0)) or 0),
"display_current_load": int(display_summary.get("current_load", 0) or 0),
"display_active_threads": int(display_summary.get("active_threads", 0) or 0),
"display_max_threads": int(display_summary.get("max_threads", 0) or 0),
"display_items_completed": int(display_summary.get("items_completed", 0) or 0),
"display_items_failed": int(display_summary.get("items_failed", 0) or 0),
"display_active_node_codes": list(display_summary.get("active_nodes") or []),
"recent_events": events,
"latest_event": events[0] if events else None,
"current_cycle_token": cycle_token,
"current_cycle_events": current_cycle_events,
"latest_cycle_event": current_cycle_events[0] if current_cycle_events else None,
}
def _enrich_active_job_summary_with_runtime(
summary: dict | None,
*,
event_limit: int = 20,
window_minutes: int = 15,
runtime_activity: dict | None = None,
runtime_snapshot: dict | None = None,
recent_domain_events: list[dict] | None = None,
) -> dict | None:
if not summary:
return summary
enriched = dict(summary)
runtime_activity = dict(runtime_activity or _load_runtime_activity_snapshot(window_minutes))
runtime_snapshot = _build_live_runtime_snapshot(runtime_snapshot or _load_latest_runtime_active_job_snapshot(window_minutes))
runtime_job_code = str(runtime_activity.get("focus_job_code") or "").strip()
runtime_job_codes = [
str(item or "").strip()
for item in list(runtime_activity.get("job_codes") or [])
if str(item or "").strip()
]
if runtime_job_code:
enriched["runtime_job_code"] = runtime_job_code
if runtime_job_codes:
enriched["runtime_job_codes"] = runtime_job_codes
enriched["processed_recent"] = max(
int(enriched.get("processed_recent", 0) or 0),
int(runtime_activity.get("processed_recent", 0) or 0),
)
enriched["processed_per_minute"] = max(
float(enriched.get("processed_per_minute", 0) or 0),
round(int(runtime_activity.get("processed_recent", 0) or 0) / max(1, int(window_minutes or 15)), 2),
)
enriched["completed_recent"] = max(
int(enriched.get("completed_recent", 0) or 0),
int(runtime_activity.get("completed_recent", 0) or 0),
)
enriched["failed_recent"] = max(
int(enriched.get("failed_recent", 0) or 0),
int(runtime_activity.get("failed_recent", 0) or 0),
)
enriched["blacklisted_recent"] = max(
int(enriched.get("blacklisted_recent", 0) or 0),
int(runtime_activity.get("blacklisted_recent", 0) or 0),
)
runtime_allow_raw_display_fallback = True
runtime_missing_participant_attribution = False
if recent_domain_events is not None:
enriched["recent_domain_events"] = list(recent_domain_events)
elif not enriched.get("recent_domain_events"):
try:
from app.services.sync_record_service import _collect_recent_domain_events
enriched["recent_domain_events"] = _collect_recent_domain_events(
enriched,
limit=max(10, min(int(event_limit or 20), 60)),
)
except Exception:
enriched["recent_domain_events"] = []
if runtime_snapshot:
runtime_job = dict(runtime_snapshot.get("job") or {})
runtime_queue = dict((runtime_snapshot.get("queue_health") or {}).get("queue") or {})
runtime_nodes = list((runtime_snapshot.get("queue_health") or {}).get("nodes") or [])
runtime_steps = list((runtime_snapshot.get("queue_health") or {}).get("steps") or [])
runtime_job_nodes = list(runtime_job.get("node_stats") or [])
runtime_snapshot_source = str(runtime_snapshot.get("_snapshot_source") or "").strip()
runtime_snapshot_stale_for_live_display = _snapshot_is_stale_for_live_display(runtime_snapshot)
if runtime_snapshot_stale_for_live_display and str(enriched.get("created_by") or "").strip() == runtime_snapshot_source:
runtime_snapshot_stale_for_live_display = False
has_runtime_participant_nodes = any(
str(item.get("node_code") or "").strip() and str(item.get("node_code") or "").strip() != "unassigned"
for item in [*runtime_nodes, *runtime_job_nodes]
if isinstance(item, dict)
)
runtime_missing_participant_attribution = bool(runtime_snapshot.get("_has_runtime_node_rows")) and not has_runtime_participant_nodes
runtime_allow_raw_display_fallback = (
int(runtime_snapshot.get("_dropped_runtime_node_count", 0) or 0) <= 0
and (
not bool(runtime_snapshot.get("_has_runtime_node_rows"))
or has_runtime_participant_nodes
)
)
if runtime_snapshot_stale_for_live_display:
runtime_missing_participant_attribution = True
runtime_allow_raw_display_fallback = False
if runtime_job and not runtime_snapshot_stale_for_live_display:
runtime_snapshot_job_id = int(runtime_job.get("job_id", 0) or 0)
enriched["runtime_snapshot_job_id"] = runtime_snapshot_job_id
runtime_snapshot_job_code = str(runtime_job.get("job_code") or "").strip()
enriched["runtime_snapshot_job_code"] = runtime_snapshot_job_code
if runtime_snapshot_job_code:
enriched["job_code"] = runtime_snapshot_job_code
enriched["runtime_job_code"] = runtime_snapshot_job_code
merged_runtime_codes = [runtime_snapshot_job_code] + [
item for item in list(enriched.get("runtime_job_codes") or []) if str(item or "").strip() != runtime_snapshot_job_code
]
enriched["runtime_job_codes"] = merged_runtime_codes[:5]
if runtime_snapshot_job_id > 0:
enriched["job_id"] = runtime_snapshot_job_id
runtime_status = str(runtime_job.get("status") or "").strip()
if runtime_status:
enriched["status"] = runtime_status
if float(runtime_job.get("progress_percent", 0) or 0) > float(enriched.get("progress_percent", 0) or 0):
enriched["progress_percent"] = float(runtime_job.get("progress_percent", 0) or 0)
if runtime_queue and not runtime_snapshot_stale_for_live_display:
enriched["runtime_snapshot_queue"] = runtime_queue
runtime_items_total = int(runtime_queue.get("items_total", 0) or 0)
if runtime_items_total > 0:
enriched["items_total"] = runtime_items_total
enriched["items_pending"] = int(runtime_queue.get("pending", 0) or 0)
enriched["items_claimed"] = int(runtime_queue.get("claimed", 0) or 0)
enriched["items_running"] = int(runtime_queue.get("running", 0) or 0)
enriched["items_completed"] = int(runtime_queue.get("completed", 0) or 0)
enriched["items_blacklisted"] = int(runtime_queue.get("blacklisted", 0) or 0)
enriched["items_failed"] = int(runtime_queue.get("failed", 0) or 0)
enriched["items_terminal"] = int(runtime_queue.get("terminal", 0) or 0)
enriched["display_items_claimed"] = _max_runtime_metric(
enriched.get("display_items_claimed"),
runtime_queue.get("display_claimed"),
enriched.get("items_claimed"),
)
display_running_candidates = [
enriched.get("display_items_running"),
runtime_queue.get("display_running"),
enriched.get("display_active_threads"),
]
if runtime_allow_raw_display_fallback:
display_running_candidates.append(enriched.get("items_running"))
enriched["display_items_running"] = _max_runtime_metric(*display_running_candidates)
if runtime_job_nodes and not runtime_snapshot_stale_for_live_display:
enriched["node_stats"] = list(runtime_job_nodes)
enriched["distributed_node_stats"] = list(runtime_job_nodes)
runtime_display_summary = _build_display_summary(list(runtime_job_nodes))
enriched["display_items_claimed"] = _max_runtime_metric(
enriched.get("display_items_claimed"),
runtime_display_summary.get("items_claimed"),
enriched.get("items_claimed"),
)
enriched["display_items_running"] = _max_runtime_metric(
enriched.get("display_items_running"),
runtime_display_summary.get("display_running"),
enriched.get("display_active_threads"),
*([] if not runtime_allow_raw_display_fallback else [enriched.get("items_running")]),
)
enriched["display_current_load"] = _max_runtime_metric(
enriched.get("display_current_load"),
runtime_display_summary.get("current_load"),
enriched.get("display_items_running"),
)
enriched["display_active_threads"] = _max_runtime_metric(
enriched.get("display_active_threads"),
runtime_display_summary.get("active_threads"),
enriched.get("display_items_running"),
*([] if not runtime_allow_raw_display_fallback else [enriched.get("items_running")]),
)
enriched["display_max_threads"] = _max_runtime_metric(
enriched.get("display_max_threads"),
runtime_display_summary.get("max_threads"),
)
enriched["display_items_completed"] = int(runtime_display_summary.get("items_completed", enriched.get("display_items_completed", 0)) or 0)
enriched["display_items_failed"] = int(runtime_display_summary.get("items_failed", enriched.get("display_items_failed", 0)) or 0)
enriched["display_active_node_codes"] = list(runtime_display_summary.get("active_nodes") or [])
elif runtime_nodes and not runtime_snapshot_stale_for_live_display:
enriched["node_stats"] = list(runtime_nodes)
enriched["distributed_node_stats"] = list(runtime_nodes)
if runtime_steps and not runtime_snapshot_stale_for_live_display:
enriched["step_stats"] = list(runtime_steps)
if not runtime_snapshot_stale_for_live_display:
runtime_recent_events = _select_runtime_snapshot_events(
runtime_snapshot,
job_code=str(runtime_job.get("job_code") or "").strip(),
job_id=int(runtime_job.get("job_id", 0) or 0),
event_limit=event_limit,
)
if runtime_recent_events:
enriched["recent_events"] = runtime_recent_events
current_cycle_token, current_cycle_events = _extract_current_cycle_events(runtime_recent_events)
enriched["current_cycle_token"] = current_cycle_token
enriched["current_cycle_events"] = current_cycle_events
enriched["latest_event"] = runtime_recent_events[0]
enriched["latest_cycle_event"] = current_cycle_events[0] if current_cycle_events else runtime_recent_events[0]
aligned_job_code = str(enriched.get("job_code") or enriched.get("runtime_job_code") or "").strip()
aligned_job_id = int(enriched.get("job_id", 0) or 0)
aligned_recent_events = _filter_events_for_job(
list(enriched.get("recent_events") or []),
job_code=aligned_job_code,
job_id=aligned_job_id,
limit=event_limit,
)
if list(enriched.get("recent_events") or []) and not aligned_recent_events:
enriched["recent_events"] = []
enriched["current_cycle_token"] = ""
enriched["current_cycle_events"] = []
enriched["latest_event"] = None
enriched["latest_cycle_event"] = None
elif aligned_recent_events:
enriched["recent_events"] = aligned_recent_events
current_cycle_token, current_cycle_events = _extract_current_cycle_events(aligned_recent_events)
enriched["current_cycle_token"] = current_cycle_token
enriched["current_cycle_events"] = current_cycle_events
enriched["latest_event"] = aligned_recent_events[0]
enriched["latest_cycle_event"] = current_cycle_events[0] if current_cycle_events else aligned_recent_events[0]
enriched["display_items_running"] = _max_runtime_metric(
enriched.get("display_items_running"),
enriched.get("display_active_threads"),
*([] if not runtime_allow_raw_display_fallback else [enriched.get("items_running")]),
)
enriched["display_current_load"] = _max_runtime_metric(
enriched.get("display_current_load"),
enriched.get("display_items_running"),
enriched.get("display_active_threads"),
*([] if not runtime_allow_raw_display_fallback else [enriched.get("items_running")]),
)
enriched["display_active_threads"] = _max_runtime_metric(
enriched.get("display_active_threads"),
enriched.get("display_items_running"),
*([] if not runtime_allow_raw_display_fallback else [enriched.get("items_running")]),
)
if runtime_missing_participant_attribution:
enriched["display_items_running"] = 0
enriched["display_current_load"] = 0
enriched["display_active_threads"] = 0
enriched["display_active_node_codes"] = []
enriched["display_max_threads"] = 0
enriched["display_max_threads"] = _max_runtime_metric(
enriched.get("display_max_threads"),
sum(
_int_value(item.get("max_threads"))
for item in list(enriched.get("distributed_node_stats") or [])
if isinstance(item, dict)
),
)
return enriched
@db_read_retry()
def get_detect_job_summary(job_id: int, event_limit: int = 20) -> dict | None:
with get_db() as conn:
with conn.cursor() as cur:
_execute_detect_jobs_select(
cur,
from_clause="FROM detect_jobs",
where_clause="id = %s",
limit_clause="1",
params=(int(job_id),),
)
row = cur.fetchone()
if not row:
return None
return _enrich_active_job_summary_with_runtime(_fetch_job_summary(cur, row, event_limit=event_limit), event_limit=event_limit)
def _select_runtime_aligned_active_job_row(
job_rows: list[tuple],
runtime_activity: dict | None = None,
runtime_snapshot: dict | None = None,
):
rows = list(job_rows or [])
if not rows:
return None
activity = dict(runtime_activity or {})
snapshot = dict(runtime_snapshot or {})
snapshot_job = dict(snapshot.get("job") or {})
preferred_job_codes = [
str(snapshot_job.get("job_code") or "").strip(),
str(activity.get("focus_job_code") or "").strip(),
*[
str(item or "").strip()
for item in list(activity.get("job_codes") or [])
if str(item or "").strip()
],
]
normalized_codes: list[str] = []
seen_codes: set[str] = set()
for item in preferred_job_codes:
if not item or item in seen_codes:
continue
normalized_codes.append(item)
seen_codes.add(item)
if not normalized_codes:
return rows[0]
for preferred_code in normalized_codes:
for row in rows:
if str(row[1] or "").strip() == preferred_code:
return row
return rows[0]
@db_read_retry()
def get_active_detect_job_summary(event_limit: int = 20) -> dict | None:
safe_event_limit = max(1, min(int(event_limit or 20), 100))
cache_key = (safe_event_limit,)
now_ts = time.monotonic()
with _ACTIVE_JOB_SUMMARY_CACHE_LOCK:
cached = _ACTIVE_JOB_SUMMARY_CACHE.get(cache_key)
if cached and now_ts < float(cached[0] or 0.0):
return _clone_cacheable_payload(cached[1])
_maybe_recycle_expired_detect_job_items()
result: dict | None = None
with get_db() as conn:
with conn.cursor() as cur:
runtime_snapshot = _load_latest_runtime_active_job_snapshot(15)
runtime_activity = _load_runtime_activity_snapshot(15)
_execute_detect_jobs_select(
cur,
from_clause="FROM detect_jobs",
where_clause="status IN ('pending', 'running')",
order_clause="""
CASE WHEN status = 'running' THEN 0 ELSE 1 END ASC,
COALESCE(started_at, created_at) DESC,
id DESC
""",
limit_clause="20",
)
rows = list(cur.fetchall() or [])
if not rows:
runtime_summary = _build_active_job_summary_from_runtime_snapshot(
runtime_snapshot,
event_limit=safe_event_limit,
)
if not runtime_summary:
result = None
else:
result = _enrich_active_job_summary_with_runtime(
runtime_summary,
event_limit=safe_event_limit,
runtime_activity=runtime_activity,
runtime_snapshot=runtime_snapshot,
)
else:
row = _select_runtime_aligned_active_job_row(
rows,
runtime_activity=runtime_activity,
runtime_snapshot=runtime_snapshot,
)
result = _enrich_active_job_summary_with_runtime(
_fetch_job_summary(cur, row, event_limit=safe_event_limit),
event_limit=safe_event_limit,
runtime_activity=runtime_activity,
runtime_snapshot=runtime_snapshot,
)
with _ACTIVE_JOB_SUMMARY_CACHE_LOCK:
_ACTIVE_JOB_SUMMARY_CACHE[cache_key] = (
time.monotonic() + _ACTIVE_JOB_SUMMARY_CACHE_TTL_SECONDS,
_clone_cacheable_payload(result),
)
return _clone_cacheable_payload(result)
@db_read_retry()
def get_latest_detect_job_summary(
*,
event_limit: int = 20,
statuses: tuple[str, ...] | list[str] | None = None,
recent_minutes: int | None = None,
) -> dict | None:
normalized_statuses = [
str(item or "").strip()
for item in list(statuses or [])
if str(item or "").strip()
]
if not normalized_statuses:
normalized_statuses = ["pending", "running", "completed", "partial_failed", "failed"]
where_clauses = ["status = ANY(%s)"]
params: list[object] = [normalized_statuses]
if recent_minutes is not None:
safe_recent_minutes = max(1, min(int(recent_minutes or 1), 24 * 60))
where_clauses.append(
"""
COALESCE(finished_at, started_at, created_at) >=
CURRENT_TIMESTAMP - (%s || ' minutes')::interval
"""
)
params.append(safe_recent_minutes)
with get_db() as conn:
with conn.cursor() as cur:
_execute_detect_jobs_select(
cur,
from_clause="FROM detect_jobs",
where_clause=" AND ".join(where_clauses),
order_clause="""
CASE
WHEN status = 'running' THEN 0
WHEN status = 'pending' THEN 1
WHEN status = 'completed' THEN 2
WHEN status = 'partial_failed' THEN 3
WHEN status = 'failed' THEN 4
ELSE 5
END ASC,
COALESCE(finished_at, started_at, created_at) DESC,
id DESC
""",
limit_clause="1",
params=tuple(params),
)
row = cur.fetchone()
if not row:
return None
return _enrich_active_job_summary_with_runtime(_fetch_job_summary(cur, row, event_limit=event_limit), event_limit=event_limit)
@db_read_retry()
def get_latest_unprojected_detect_job_summary(
*,
event_limit: int = 20,
statuses: tuple[str, ...] | list[str] | None = None,
recent_minutes: int | None = None,
source_region: str | None = None,
target_region: str | None = None,
) -> dict | None:
normalized_statuses = [
str(item or "").strip()
for item in list(statuses or [])
if str(item or "").strip()
]
if not normalized_statuses:
normalized_statuses = ["completed", "partial_failed", "failed"]
normalized_source_region = _normalize_sync_region(
source_region,
_normalize_sync_region(settings.sync_source_region, settings.node_region),
)
normalized_target_region = _normalize_sync_region(
target_region,
_normalize_sync_region(settings.sync_target_region, "overseas"),
)
where_clauses = [
"job.status = ANY(%s)",
"""
NOT EXISTS (
SELECT 1
FROM detect_sync_records AS sync_record
WHERE sync_record.sync_type = 'detect_result_projection'
AND sync_record.source_region = %s
AND sync_record.target_region = %s
AND (sync_record.payload_json->'projection'->'job'->>'job_id') = job.id::text
)
""",
]
params: list[object] = [normalized_statuses, normalized_source_region, normalized_target_region]
if recent_minutes is not None:
safe_recent_minutes = max(1, min(int(recent_minutes or 1), 24 * 60))
where_clauses.append(
"""
COALESCE(job.finished_at, job.started_at, job.created_at) >=
CURRENT_TIMESTAMP - (%s || ' minutes')::interval
"""
)
params.append(safe_recent_minutes)
with get_db() as conn:
with conn.cursor() as cur:
_execute_detect_jobs_select(
cur,
from_clause="FROM detect_jobs AS job",
where_clause=" AND ".join(where_clauses),
order_clause="""
COALESCE(job.finished_at, job.started_at, job.created_at) DESC,
job.id DESC
""",
limit_clause="1",
params=tuple(params),
alias="job",
)
row = cur.fetchone()
if not row:
return None
return _enrich_active_job_summary_with_runtime(_fetch_job_summary(cur, row, event_limit=event_limit), event_limit=event_limit)
def list_detect_jobs(limit: int = 20) -> list[dict]:
with get_db() as conn:
with conn.cursor() as cur:
_execute_detect_jobs_select(
cur,
from_clause="FROM detect_jobs",
order_clause="created_at DESC, id DESC",
limit_clause="%s",
params=(max(1, min(int(limit or 20), 100)),),
)
rows = cur.fetchall()
return [_fetch_job_summary(cur, row, event_limit=10) for row in rows]
def list_recent_detect_run_events(limit: int = 20) -> list[dict]:
safe_limit = max(1, min(int(limit or 20), 200))
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT job_id, node_code, event_type, level, message, payload_json, created_at
FROM detect_run_events
ORDER BY created_at DESC, id DESC
LIMIT %s
""",
(safe_limit,),
)
rows = cur.fetchall()
return [
{
"job_id": int(row[0]) if row[0] is not None else None,
"node_code": row[1] or "",
"event_type": row[2] or "",
"level": row[3] or "info",
"message": row[4] or "",
"payload": _decode_payload(row[5]),
"created_at": _format_time(row[6]),
}
for row in rows
]
@db_read_retry()
def get_detect_queue_health(window_minutes: int = 15) -> dict:
window_minutes = max(5, min(int(window_minutes or 15), 120))
cache_key = (window_minutes,)
now_ts = time.monotonic()
with _DETECT_QUEUE_HEALTH_CACHE_LOCK:
cached = _DETECT_QUEUE_HEALTH_CACHE.get(cache_key)
if cached and now_ts < float(cached[0] or 0.0):
return _clone_cacheable_payload(cached[1])
active_job = get_active_detect_job_summary(event_limit=10)
runtime_activity = _load_runtime_activity_snapshot(window_minutes)
runtime_snapshot = _load_latest_runtime_active_job_snapshot(window_minutes)
active_job_runtime_code = str(
(active_job or {}).get("runtime_snapshot_job_code")
or (active_job or {}).get("runtime_job_code")
or (active_job or {}).get("job_code")
or ""
).strip()
if active_job_runtime_code:
merged_runtime_codes = [active_job_runtime_code] + [
str(item or "").strip()
for item in list(runtime_activity.get("job_codes") or [])
if str(item or "").strip() and str(item or "").strip() != active_job_runtime_code
]
runtime_activity = {
**dict(runtime_activity or {}),
"focus_job_code": active_job_runtime_code,
"job_codes": merged_runtime_codes[:5],
}
runtime_snapshot_job = dict((runtime_snapshot or {}).get("job") or {})
runtime_snapshot_matches_active_job = _runtime_job_matches_active_job(
active_job,
runtime_job_code=runtime_snapshot_job.get("job_code"),
runtime_job_id=runtime_snapshot_job.get("job_id"),
)
runtime_activity_matches_active_job = _runtime_job_matches_active_job(
active_job,
runtime_job_code=(runtime_activity or {}).get("focus_job_code"),
)
if runtime_snapshot and not runtime_snapshot_matches_active_job:
runtime_snapshot = {}
if runtime_activity and not runtime_activity_matches_active_job and not runtime_snapshot_matches_active_job:
runtime_activity = {
**dict(runtime_activity or {}),
"focus_job_code": "",
"job_codes": [],
"processed_recent": 0,
"completed_recent": 0,
"failed_recent": 0,
"blacklisted_recent": 0,
"step_code": "",
"step_stats": {},
"nodes": {},
}
if not active_job:
result = {
"window_minutes": window_minutes,
"has_active_job": False,
"job": None,
"queue": {
"items_total": 0,
"pending": 0,
"claimed": 0,
"running": 0,
"completed": 0,
"blacklisted": 0,
"failed": 0,
"terminal": 0,
"terminal_percent": 0,
"oldest_pending_at": "",
"oldest_pending_age_minutes": 0,
"nearest_lease_expiry_at": "",
"overdue_leases": 0,
"expiring_soon_leases": 0,
},
"throughput": {
"processed_recent": int(runtime_activity.get("processed_recent", 0) or 0),
"processed_per_minute": round(int(runtime_activity.get("processed_recent", 0) or 0) / window_minutes, 2),
"completed_recent": int(runtime_activity.get("completed_recent", 0) or 0),
"blacklisted_recent": int(runtime_activity.get("blacklisted_recent", 0) or 0),
"failed_recent": int(runtime_activity.get("failed_recent", 0) or 0),
},
"nodes": [],
"steps": [],
"runtime_activity": runtime_activity,
}
with _DETECT_QUEUE_HEALTH_CACHE_LOCK:
_DETECT_QUEUE_HEALTH_CACHE[cache_key] = (
time.monotonic() + _DETECT_QUEUE_HEALTH_CACHE_TTL_SECONDS,
_clone_cacheable_payload(result),
)
return _clone_cacheable_payload(result)
job_id = int(active_job["job_id"])
with get_db() as conn:
with conn.cursor() as cur:
cur.execute(
"""
SELECT
MIN(create_time) FILTER (WHERE status = 'pending') AS oldest_pending_at,
MIN(lease_expires_at) FILTER (WHERE status IN ('claimed', 'running') AND lease_expires_at IS NOT NULL) AS nearest_lease_expiry_at,
COUNT(*) FILTER (WHERE status IN ('claimed', 'running') AND lease_expires_at IS NOT NULL AND lease_expires_at < CURRENT_TIMESTAMP) AS overdue_leases,
COUNT(*) FILTER (
WHERE status IN ('claimed', 'running')
AND lease_expires_at IS NOT NULL
AND lease_expires_at >= CURRENT_TIMESTAMP
AND lease_expires_at < CURRENT_TIMESTAMP + interval '5 minutes'
) AS expiring_soon_leases
FROM detect_job_items
WHERE job_id = %s
""",
(job_id,),
)
lease_row = cur.fetchone()
cur.execute(
"""
SELECT
COALESCE(NULLIF(node_code, ''), 'unknown') AS node_code,
COUNT(*) AS processed_recent,
COUNT(*) FILTER (WHERE event_type = 'domain_completed') AS completed_recent,
COUNT(*) FILTER (WHERE event_type = 'domain_blacklisted') AS blacklisted_recent,
COUNT(*) FILTER (WHERE event_type = 'domain_failed') AS failed_recent
FROM detect_run_events
WHERE job_id = %s
AND event_type IN ('domain_completed', 'domain_blacklisted', 'domain_failed')
AND created_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
GROUP BY COALESCE(NULLIF(node_code, ''), 'unknown')
ORDER BY processed_recent DESC, node_code ASC
""",
(job_id, window_minutes),
)
throughput_rows = cur.fetchall()
cur.execute(
"""
SELECT
COALESCE(NULLIF(step_code, ''), 'domain_pipeline') AS step_code,
COUNT(*) FILTER (
WHERE status IN ('completed', 'blacklisted', 'failed')
AND finished_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
) AS processed_recent,
COUNT(*) FILTER (
WHERE status = 'completed'
AND finished_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
) AS completed_recent,
COUNT(*) FILTER (
WHERE status = 'blacklisted'
AND finished_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
) AS blacklisted_recent,
COUNT(*) FILTER (
WHERE status = 'failed'
AND finished_at >= CURRENT_TIMESTAMP - (%s || ' minutes')::interval
) AS failed_recent
FROM detect_job_items
WHERE job_id = %s
GROUP BY COALESCE(NULLIF(step_code, ''), 'domain_pipeline')
ORDER BY step_code ASC
""",
(window_minutes, window_minutes, window_minutes, window_minutes, job_id),
)
step_throughput_rows = cur.fetchall()
runtime_display_rows = _load_runtime_display_rows(cur)
oldest_pending_at = _format_time(lease_row[0]) if lease_row and lease_row[0] else ""
nearest_lease_expiry_at = _format_time(lease_row[1]) if lease_row and lease_row[1] else ""
oldest_pending_age_minutes = 0
if lease_row and lease_row[0]:
oldest_pending_age_minutes = max(0, int((datetime.now() - lease_row[0]).total_seconds() // 60))
node_map = {
str(item.get("node_code") or "unknown"): {
"node_code": str(item.get("node_code") or "unknown"),
"items_total": int(item.get("items_total", 0) or 0),
"items_pending": int(item.get("items_pending", 0) or 0),
"items_claimed": int(item.get("items_claimed", 0) or 0),
"items_running": int(item.get("items_running", 0) or 0),
"items_completed": int(item.get("items_completed", 0) or 0),
"items_blacklisted": int(item.get("items_blacklisted", 0) or 0),
"items_failed": int(item.get("items_failed", 0) or 0),
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"display_running": _int_value(item.get("display_running")),
"current_load": _int_value(item.get("current_load")),
"active_threads": _int_value(item.get("active_threads")),
"max_threads": _int_value(item.get("max_threads")),
"region": str(item.get("region") or "").strip(),
"role": str(item.get("role") or "").strip(),
"status": str(item.get("status") or "").strip(),
"last_heartbeat_at": str(item.get("last_heartbeat_at") or "").strip(),
}
for item in active_job.get("node_stats") or []
}
distributed_nodes = list(active_job.get("distributed_node_stats") or [])
if distributed_nodes:
node_map = {
str(item.get("node_code") or "unknown"): {
"node_code": str(item.get("node_code") or "unknown"),
"items_total": _int_value(item.get("items_total")),
"items_pending": _int_value(item.get("items_pending")),
"items_claimed": _int_value(item.get("items_claimed")),
"items_running": _int_value(item.get("items_running")),
"items_completed": _int_value(item.get("items_completed")),
"items_blacklisted": _int_value(item.get("items_blacklisted")),
"items_failed": _int_value(item.get("items_failed")),
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": str(item.get("metrics_source") or "").strip(),
"display_running": _int_value(item.get("display_running")),
"current_load": _int_value(item.get("current_load")),
"active_threads": _int_value(item.get("active_threads")),
"max_threads": _int_value(item.get("max_threads")),
"region": str(item.get("region") or "").strip(),
"role": str(item.get("role") or "").strip(),
"status": str(item.get("status") or "").strip(),
"last_heartbeat_at": str(item.get("last_heartbeat_at") or "").strip(),
}
for item in distributed_nodes
}
total_processed_recent = 0
total_completed_recent = 0
total_blacklisted_recent = 0
total_failed_recent = 0
step_map = {
str(item.get("step_code") or "domain_pipeline"): {
**_build_step_bucket(str(item.get("step_code") or "domain_pipeline")),
"items_total": _int_value(item.get("items_total")),
"items_pending": _int_value(item.get("items_pending")),
"items_claimed": _int_value(item.get("items_claimed")),
"items_running": _int_value(item.get("items_running")),
"items_completed": _int_value(item.get("items_completed")),
"items_blacklisted": _int_value(item.get("items_blacklisted")),
"items_failed": _int_value(item.get("items_failed")),
}
for item in active_job.get("step_stats") or []
}
for row in throughput_rows:
node_code = str(row[0] or "unknown")
bucket = node_map.setdefault(
node_code,
{
"node_code": node_code,
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
},
)
processed_recent = int(row[1] or 0)
completed_recent = int(row[2] or 0)
blacklisted_recent = int(row[3] or 0)
failed_recent = int(row[4] or 0)
bucket["processed_recent"] = processed_recent
bucket["processed_per_minute"] = round(processed_recent / window_minutes, 2)
bucket["completed_recent"] = completed_recent
bucket["blacklisted_recent"] = blacklisted_recent
bucket["failed_recent"] = failed_recent
total_processed_recent += processed_recent
total_completed_recent += completed_recent
total_blacklisted_recent += blacklisted_recent
total_failed_recent += failed_recent
for row in list(runtime_display_rows or []):
runtime_bucket = _build_runtime_display_bucket(row)
if not runtime_bucket:
continue
node_code = str(runtime_bucket.get("node_code") or "unknown")
bucket = node_map.setdefault(
node_code,
{
"node_code": node_code,
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": "runtime",
"display_running": 0,
"current_load": 0,
"active_threads": 0,
"max_threads": 0,
"region": "",
"role": "",
"status": "",
"last_heartbeat_at": "",
},
)
runtime_display_running = _effective_runtime_load(
items_running=runtime_bucket.get("items_running"),
active_threads=runtime_bucket.get("active_threads"),
)
for key in (
"items_total",
"items_pending",
"items_claimed",
"items_completed",
"items_blacklisted",
"items_failed",
"current_load",
"active_threads",
"max_threads",
):
bucket[key] = max(_int_value(bucket.get(key)), _int_value(runtime_bucket.get(key)))
bucket["display_running"] = max(
_int_value(bucket.get("display_running")),
runtime_display_running,
)
bucket["items_running"] = max(
_int_value(bucket.get("items_running")),
_int_value(runtime_bucket.get("items_running")),
)
for key in ("metrics_source", "region", "role", "status", "last_heartbeat_at"):
if str(runtime_bucket.get(key) or "").strip():
bucket[key] = runtime_bucket.get(key)
for node_code, runtime_bucket in dict(runtime_activity.get("nodes") or {}).items():
bucket = node_map.setdefault(
node_code,
{
"node_code": node_code,
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": "runtime",
"display_running": 0,
"current_load": 0,
"active_threads": 0,
"max_threads": 0,
"region": "",
"role": "",
"status": "",
"last_heartbeat_at": "",
},
)
runtime_processed_recent = int(runtime_bucket.get("processed_recent", 0) or 0)
if runtime_processed_recent > int(bucket.get("processed_recent", 0) or 0):
bucket["processed_recent"] = runtime_processed_recent
bucket["processed_per_minute"] = round(runtime_processed_recent / window_minutes, 2)
bucket["completed_recent"] = int(runtime_bucket.get("completed_recent", 0) or 0)
bucket["blacklisted_recent"] = int(runtime_bucket.get("blacklisted_recent", 0) or 0)
bucket["failed_recent"] = int(runtime_bucket.get("failed_recent", 0) or 0)
for row in step_throughput_rows:
step_code = str(row[0] or "domain_pipeline")
bucket = step_map.setdefault(step_code, _build_step_bucket(step_code))
processed_recent = int(row[1] or 0)
completed_recent = int(row[2] or 0)
blacklisted_recent = int(row[3] or 0)
failed_recent = int(row[4] or 0)
bucket["processed_recent"] = processed_recent
bucket["processed_per_minute"] = round(processed_recent / window_minutes, 2)
bucket["completed_recent"] = completed_recent
bucket["blacklisted_recent"] = blacklisted_recent
bucket["failed_recent"] = failed_recent
runtime_step_stats = dict(runtime_activity.get("step_stats") or {})
for runtime_step_code, runtime_step_bucket in runtime_step_stats.items():
normalized_step_code = str(runtime_step_code or "").strip()
if not normalized_step_code:
continue
bucket = step_map.setdefault(normalized_step_code, _build_step_bucket(normalized_step_code))
runtime_processed_recent = int(runtime_step_bucket.get("processed_recent", 0) or 0)
runtime_started_recent = int(runtime_step_bucket.get("started_recent", 0) or 0)
bucket["started_recent"] = max(int(bucket.get("started_recent", 0) or 0), runtime_started_recent)
if runtime_processed_recent > int(bucket.get("processed_recent", 0) or 0):
bucket["processed_recent"] = runtime_processed_recent
bucket["processed_per_minute"] = round(runtime_processed_recent / window_minutes, 2)
bucket["completed_recent"] = int(runtime_step_bucket.get("completed_recent", 0) or 0)
bucket["blacklisted_recent"] = int(runtime_step_bucket.get("blacklisted_recent", 0) or 0)
bucket["failed_recent"] = int(runtime_step_bucket.get("failed_recent", 0) or 0)
elif int(bucket.get("processed_recent", 0) or 0) <= 0 and runtime_started_recent > 0:
bucket["processed_recent"] = runtime_started_recent
bucket["processed_per_minute"] = round(runtime_started_recent / window_minutes, 2)
runtime_step_code = str(runtime_activity.get("step_code") or "").strip()
runtime_processed_recent = int(runtime_activity.get("processed_recent", 0) or 0)
if runtime_step_code and runtime_processed_recent > 0 and runtime_step_code not in runtime_step_stats:
runtime_bucket = step_map.setdefault(runtime_step_code, _build_step_bucket(runtime_step_code))
if runtime_processed_recent > int(runtime_bucket.get("processed_recent", 0) or 0):
runtime_bucket["processed_recent"] = runtime_processed_recent
runtime_bucket["processed_per_minute"] = round(runtime_processed_recent / window_minutes, 2)
runtime_bucket["completed_recent"] = int(runtime_activity.get("completed_recent", 0) or 0)
runtime_bucket["blacklisted_recent"] = int(runtime_activity.get("blacklisted_recent", 0) or 0)
runtime_bucket["failed_recent"] = int(runtime_activity.get("failed_recent", 0) or 0)
total_processed_recent = max(total_processed_recent, int(runtime_activity.get("processed_recent", 0) or 0))
total_completed_recent = max(total_completed_recent, int(runtime_activity.get("completed_recent", 0) or 0))
total_blacklisted_recent = max(total_blacklisted_recent, int(runtime_activity.get("blacklisted_recent", 0) or 0))
total_failed_recent = max(total_failed_recent, int(runtime_activity.get("failed_recent", 0) or 0))
runtime_snapshot_job = dict(runtime_snapshot.get("job") or {})
runtime_snapshot_queue = dict((runtime_snapshot.get("queue_health") or {}).get("queue") or {})
runtime_snapshot_nodes = list((runtime_snapshot.get("queue_health") or {}).get("nodes") or [])
runtime_snapshot_steps = list((runtime_snapshot.get("queue_health") or {}).get("steps") or [])
runtime_snapshot_job_code = str(runtime_snapshot_job.get("job_code") or "").strip()
items_total = int(active_job.get("items_total", 0) or 0)
pending_items = int(active_job.get("items_pending", 0) or 0)
claimed_items = int(active_job.get("items_claimed", 0) or 0)
running_items = int(active_job.get("items_running", 0) or 0)
completed_items = int(active_job.get("items_completed", 0) or 0)
blacklisted_items = int(active_job.get("items_blacklisted", 0) or 0)
failed_items = int(active_job.get("items_failed", 0) or 0)
display_claimed = int(active_job.get("display_items_claimed", active_job.get("items_claimed", 0)) or 0)
display_running = int(
active_job.get("display_active_threads", active_job.get("display_items_running", 0))
or 0
)
progress_percent = float(active_job.get("progress_percent", 0) or 0)
if runtime_snapshot_queue:
runtime_items_total = int(runtime_snapshot_queue.get("items_total", 0) or 0)
if runtime_items_total > 0:
items_total = runtime_items_total
pending_items = int(runtime_snapshot_queue.get("pending", 0) or 0)
claimed_items = int(runtime_snapshot_queue.get("claimed", 0) or 0)
running_items = int(runtime_snapshot_queue.get("running", 0) or 0)
completed_items = int(runtime_snapshot_queue.get("completed", 0) or 0)
blacklisted_items = int(runtime_snapshot_queue.get("blacklisted", 0) or 0)
failed_items = int(runtime_snapshot_queue.get("failed", 0) or 0)
display_claimed = int(runtime_snapshot_queue.get("display_claimed", claimed_items) or 0)
display_running = int(runtime_snapshot_queue.get("display_running", display_running) or 0)
progress_percent = float(runtime_snapshot_job.get("progress_percent", progress_percent) or 0)
if runtime_snapshot_nodes:
node_map = {
str(item.get("node_code") or "unknown"): {
"node_code": str(item.get("node_code") or "unknown"),
"items_total": _int_value(item.get("items_total")),
"items_pending": _int_value(item.get("items_pending")),
"items_claimed": _int_value(item.get("items_claimed")),
"items_running": _int_value(item.get("items_running")),
"items_completed": _int_value(item.get("items_completed")),
"items_blacklisted": _int_value(item.get("items_blacklisted")),
"items_failed": _int_value(item.get("items_failed")),
"processed_recent": _int_value(item.get("processed_recent")),
"processed_per_minute": float(item.get("processed_per_minute", 0) or 0),
"completed_recent": _int_value(item.get("completed_recent")),
"blacklisted_recent": _int_value(item.get("blacklisted_recent")),
"failed_recent": _int_value(item.get("failed_recent")),
"metrics_source": str(item.get("metrics_source") or "").strip(),
"display_running": _int_value(item.get("display_running")),
"current_load": _int_value(item.get("current_load")),
"active_threads": _int_value(item.get("active_threads")),
"max_threads": _int_value(item.get("max_threads")),
"region": str(item.get("region") or "").strip(),
"role": str(item.get("role") or "").strip(),
"status": str(item.get("status") or "").strip(),
"last_heartbeat_at": str(item.get("last_heartbeat_at") or "").strip(),
}
for item in runtime_snapshot_nodes
}
if runtime_snapshot_steps:
step_map = {
str(item.get("step_code") or "domain_pipeline"): {
**_build_step_bucket(str(item.get("step_code") or "domain_pipeline")),
"items_total": _int_value(item.get("items_total")),
"items_pending": _int_value(item.get("items_pending")),
"items_claimed": _int_value(item.get("items_claimed")),
"items_running": _int_value(item.get("items_running")),
"items_completed": _int_value(item.get("items_completed")),
"items_blacklisted": _int_value(item.get("items_blacklisted")),
"items_failed": _int_value(item.get("items_failed")),
"processed_recent": _int_value(item.get("processed_recent")),
"processed_per_minute": float(item.get("processed_per_minute", 0) or 0),
"completed_recent": _int_value(item.get("completed_recent")),
"blacklisted_recent": _int_value(item.get("blacklisted_recent")),
"failed_recent": _int_value(item.get("failed_recent")),
}
for item in runtime_snapshot_steps
}
for row in list(runtime_display_rows or []):
runtime_bucket = _build_runtime_display_bucket(row)
if not runtime_bucket:
continue
node_code = str(runtime_bucket.get("node_code") or "unknown")
bucket = node_map.setdefault(
node_code,
{
"node_code": node_code,
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": "runtime",
"display_running": 0,
"current_load": 0,
"active_threads": 0,
"max_threads": 0,
"region": "",
"role": "",
"status": "",
"last_heartbeat_at": "",
},
)
runtime_display_running = _effective_runtime_load(
items_running=runtime_bucket.get("items_running"),
active_threads=runtime_bucket.get("active_threads"),
)
for key in (
"items_total",
"items_pending",
"items_claimed",
"items_completed",
"items_blacklisted",
"items_failed",
"current_load",
"active_threads",
"max_threads",
):
bucket[key] = max(_int_value(bucket.get(key)), _int_value(runtime_bucket.get(key)))
bucket["display_running"] = max(
_int_value(bucket.get("display_running")),
runtime_display_running,
)
bucket["items_running"] = max(
_int_value(bucket.get("items_running")),
_int_value(runtime_bucket.get("items_running")),
)
for key in ("metrics_source", "region", "role", "status", "last_heartbeat_at"):
if str(runtime_bucket.get(key) or "").strip():
bucket[key] = runtime_bucket.get(key)
for node_code, runtime_bucket in dict(runtime_activity.get("nodes") or {}).items():
bucket = node_map.setdefault(
node_code,
{
"node_code": node_code,
"items_total": 0,
"items_pending": 0,
"items_claimed": 0,
"items_running": 0,
"items_completed": 0,
"items_blacklisted": 0,
"items_failed": 0,
"processed_recent": 0,
"processed_per_minute": 0,
"completed_recent": 0,
"blacklisted_recent": 0,
"failed_recent": 0,
"metrics_source": "runtime",
},
)
runtime_processed_recent = int(runtime_bucket.get("processed_recent", 0) or 0)
if runtime_processed_recent > int(bucket.get("processed_recent", 0) or 0):
bucket["processed_recent"] = runtime_processed_recent
bucket["processed_per_minute"] = round(runtime_processed_recent / window_minutes, 2)
bucket["completed_recent"] = int(runtime_bucket.get("completed_recent", 0) or 0)
bucket["blacklisted_recent"] = int(runtime_bucket.get("blacklisted_recent", 0) or 0)
bucket["failed_recent"] = int(runtime_bucket.get("failed_recent", 0) or 0)
if not str(bucket.get("metrics_source") or "").strip():
bucket["metrics_source"] = "runtime"
for bucket in node_map.values():
active_threads = int(bucket.get("active_threads", 0) or 0)
current_load = int(bucket.get("current_load", 0) or 0)
display_running_value = max(
int(bucket.get("display_running", 0) or 0),
active_threads,
current_load,
)
bucket["display_running"] = display_running_value
if active_threads > 0:
bucket["items_running"] = min(
max(int(bucket.get("items_running", 0) or 0), active_threads),
active_threads,
)
bucket["current_load"] = max(current_load, active_threads)
for bucket in step_map.values():
runtime_bucket = dict(runtime_activity.get("step_stats") or {}).get(str(bucket.get("step_code") or "").strip()) or {}
if runtime_bucket:
bucket["started_recent"] = max(
int(bucket.get("started_recent", 0) or 0),
int(runtime_bucket.get("started_recent", 0) or 0),
)
runtime_processed_recent = int(runtime_bucket.get("processed_recent", 0) or 0)
if runtime_processed_recent > int(bucket.get("processed_recent", 0) or 0):
bucket["processed_recent"] = runtime_processed_recent
bucket["processed_per_minute"] = round(runtime_processed_recent / window_minutes, 2)
bucket["completed_recent"] = int(runtime_bucket.get("completed_recent", 0) or 0)
bucket["blacklisted_recent"] = int(runtime_bucket.get("blacklisted_recent", 0) or 0)
bucket["failed_recent"] = int(runtime_bucket.get("failed_recent", 0) or 0)
snapshot_created_at = _snapshot_created_at(runtime_snapshot)
raw_nodes = list(node_map.values())
live_nodes = _filter_live_runtime_nodes(raw_nodes, snapshot_created_at=snapshot_created_at)
raw_participant_count = sum(
1
for item in raw_nodes
if str(item.get("node_code") or "").strip()
and str(item.get("node_code") or "").strip() != "unassigned"
)
live_participant_count = sum(
1
for item in live_nodes
if str(item.get("node_code") or "").strip()
and str(item.get("node_code") or "").strip() != "unassigned"
)
dropped_runtime_node_count = max(0, raw_participant_count - live_participant_count)
if raw_nodes:
node_map = {
str(item.get("node_code") or "unknown"): dict(item)
for item in live_nodes
}
normalized_running_items = sum(
int(item.get("items_running", 0) or 0)
for item in node_map.values()
if str(item.get("node_code") or "").strip() and str(item.get("node_code") or "").strip() != "unassigned"
)
normalized_display_running = sum(
int(item.get("display_running", 0) or 0)
for item in node_map.values()
if str(item.get("node_code") or "").strip() and str(item.get("node_code") or "").strip() != "unassigned"
)
# Prefer normalized per-node sums once runtime heartbeats are present.
# Stale snapshot queue rows can temporarily over-report thread load from an
# older cycle, which is exactly what misleads the ops dashboard.
if normalized_running_items > 0 or dropped_runtime_node_count > 0:
running_items = normalized_running_items
if normalized_display_running > 0 or dropped_runtime_node_count > 0:
display_running = normalized_display_running
nodes = sorted(
node_map.values(),
key=lambda item: (
-int(item.get("processed_recent", 0) or 0),
-int(item.get("display_running", item.get("items_running", 0)) or 0),
-int(item.get("items_running", 0) or 0),
str(item.get("node_code") or ""),
),
)
terminal = completed_items + blacklisted_items + failed_items
steps = order_step_buckets(list(step_map.values()))
result = {
"window_minutes": window_minutes,
"has_active_job": True,
"job": {
"job_id": job_id,
"job_code": active_job.get("job_code", ""),
"runtime_job_code": runtime_snapshot_job_code or str(runtime_activity.get("focus_job_code") or "").strip(),
"runtime_job_codes": list(runtime_activity.get("job_codes") or []),
"status": active_job.get("status", ""),
"progress_percent": progress_percent,
},
"queue": {
"items_total": items_total,
"pending": pending_items,
"claimed": claimed_items,
"running": running_items,
"display_claimed": display_claimed,
"display_running": display_running,
"completed": completed_items,
"blacklisted": blacklisted_items,
"failed": failed_items,
"terminal": terminal,
"terminal_percent": round((terminal / items_total) * 100, 2) if items_total else 0,
"oldest_pending_at": oldest_pending_at,
"oldest_pending_age_minutes": oldest_pending_age_minutes,
"nearest_lease_expiry_at": nearest_lease_expiry_at,
"overdue_leases": int(lease_row[2] or 0) if lease_row else 0,
"expiring_soon_leases": int(lease_row[3] or 0) if lease_row else 0,
},
"throughput": {
"processed_recent": total_processed_recent,
"processed_per_minute": round(total_processed_recent / window_minutes, 2),
"completed_recent": total_completed_recent,
"blacklisted_recent": total_blacklisted_recent,
"failed_recent": total_failed_recent,
},
"nodes": nodes,
"steps": steps,
"runtime_activity": runtime_activity,
"runtime_snapshot_backlog": dict(runtime_snapshot.get("backlog") or {}),
}
with _DETECT_QUEUE_HEALTH_CACHE_LOCK:
_DETECT_QUEUE_HEALTH_CACHE[cache_key] = (
time.monotonic() + _DETECT_QUEUE_HEALTH_CACHE_TTL_SECONDS,
_clone_cacheable_payload(result),
)
return _clone_cacheable_payload(result)
def get_detect_capacity_plan(*, queue_health: dict | None = None, online_worker_nodes: int = 0, target_finish_hours: int = 6) -> dict:
queue_health = queue_health or get_detect_queue_health(window_minutes=15)
target_finish_hours = max(1, min(int(target_finish_hours or 6), 72))
online_worker_nodes = max(0, int(online_worker_nodes or 0))
if not queue_health.get("has_active_job"):
return {
"has_active_job": False,
"online_worker_nodes": online_worker_nodes,
"target_finish_hours": target_finish_hours,
"estimated_hours_remaining": 0,
"recommended_total_workers": max(1, online_worker_nodes),
"recommended_additional_workers": 0,
"current_processed_per_hour": 0,
"pending_items": 0,
"terminal_items": 0,
"summary": "当前没有活跃任务,无需扩容建议。",
}
queue = queue_health.get("queue") or {}
throughput = queue_health.get("throughput") or {}
pending_items = int(queue.get("pending", 0) or 0)
claimed_items = int(queue.get("claimed", 0) or 0)
running_items = int(queue.get("running", 0) or 0)
remaining_items = pending_items + claimed_items + running_items
current_processed_per_hour = round(float(throughput.get("processed_per_minute", 0) or 0) * 60, 2)
estimated_hours_remaining = round((remaining_items / current_processed_per_hour), 2) if current_processed_per_hour > 0 else 0
recommended_total_workers = max(1, online_worker_nodes or 1)
recommended_additional_workers = 0
if remaining_items > 0 and target_finish_hours > 0:
required_per_hour = remaining_items / target_finish_hours
if current_processed_per_hour > 0 and max(1, online_worker_nodes) > 0:
per_worker_per_hour = current_processed_per_hour / max(1, online_worker_nodes)
recommended_total_workers = max(1, int(math.ceil(required_per_hour / per_worker_per_hour)))
recommended_additional_workers = max(0, recommended_total_workers - online_worker_nodes)
elif remaining_items > 0:
recommended_total_workers = max(1, online_worker_nodes or 1)
recommended_additional_workers = 0
summary = (
f"当前在线 Worker {online_worker_nodes} 台,近窗吞吐约 {current_processed_per_hour} 项/小时,"
f"剩余待处理约 {remaining_items} 项,预计还需 {estimated_hours_remaining} 小时。"
)
if recommended_additional_workers > 0:
summary = (
f"{summary} 若希望在 {target_finish_hours} 小时内收敛,建议总 Worker 数达到 "
f"{recommended_total_workers} 台,至少再加 {recommended_additional_workers} 台。"
)
else:
summary = f"{summary} 按当前目标 {target_finish_hours} 小时看,现有 Worker 数量暂时够用。"
return {
"has_active_job": True,
"online_worker_nodes": online_worker_nodes,
"target_finish_hours": target_finish_hours,
"estimated_hours_remaining": estimated_hours_remaining,
"recommended_total_workers": recommended_total_workers,
"recommended_additional_workers": recommended_additional_workers,
"current_processed_per_hour": current_processed_per_hour,
"pending_items": pending_items,
"remaining_items": remaining_items,
"terminal_items": int(queue.get("terminal", 0) or 0),
"summary": summary,
}
def process_detect_pipeline(limit: int = 5000, job_id: int | None = None) -> dict:
safe_limit = max(1, min(int(limit or 5000), 5000))
normalized_job_id = int(job_id or 0) if job_id not in (None, "", 0, "0") else 0
summary = {
"processed_items": 0,
"advanced_items": 0,
"retried_items": 0,
"terminal_items": 0,
"skipped_items": 0,
"created_next_items": 0,
"retry_reset_items": 0,
"legacy_items_upgraded": 0,
"legacy_items_completed": 0,
"job_ids": [],
}
with get_db() as conn:
conn.autocommit = False
with conn.cursor() as cur:
settings_payload = get_settings_payload()
touched_job_ids: set[int] = set()
cur.execute(
"""
SELECT item.id, item.job_id, item.domain_id
FROM detect_job_items AS item
JOIN detect_jobs AS job
ON job.id = item.job_id
WHERE job.task_mode = 'domain_pipeline'
AND (%s = 0 OR item.job_id = %s)
AND job.status IN ('pending', 'running', 'completed', 'failed', 'partial_failed')
AND COALESCE(item.step_code, '') = ''
AND item.status = 'pending'
ORDER BY item.id ASC
LIMIT %s
FOR UPDATE SKIP LOCKED
""",
(normalized_job_id, normalized_job_id, safe_limit),
)
legacy_rows = cur.fetchall()
for row in legacy_rows:
item_id = int(row[0])
legacy_job_id = int(row[1])
domain_id = int(row[2])
domain_snapshot = _load_domain_pipeline_snapshot(cur, domain_id)
if not domain_snapshot:
continue
step_code, step_payload = resolve_initial_domain_pipeline_item(
domain_snapshot,
settings_payload=settings_payload,
)
if step_code and step_payload:
cur.execute(
"""
UPDATE detect_job_items
SET step_code = %s,
step_payload_json = %s::jsonb,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""",
(step_code, json.dumps(step_payload, ensure_ascii=False), item_id),
)
touched_job_ids.add(legacy_job_id)
summary["processed_items"] += 1
summary["legacy_items_upgraded"] += 1
continue
processed_payload = {
"controller_processed": True,
"controller_action": "pipeline_already_complete",
"controller_processed_at": datetime.now().isoformat(timespec="seconds"),
}
cur.execute(
"""
UPDATE detect_job_items
SET status = 'completed',
finished_at = COALESCE(finished_at, CURRENT_TIMESTAMP),
result_payload_json = %s::jsonb,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""",
(json.dumps(processed_payload, ensure_ascii=False), item_id),
)
touched_job_ids.add(legacy_job_id)
summary["processed_items"] += 1
summary["legacy_items_completed"] += 1
cur.execute(
"""
SELECT item.id, item.job_id, item.domain_id, item.step_code, item.status, item.attempt_count, item.result_payload_json, job.task_mode
FROM detect_job_items AS item
JOIN detect_jobs AS job
ON job.id = item.job_id
WHERE job.task_mode IN ('domain_pipeline', 'single_step')
AND (%s = 0 OR item.job_id = %s)
AND job.status IN ('pending', 'running', 'completed', 'failed', 'partial_failed')
AND COALESCE(item.step_code, '') <> ''
AND item.status IN ('completed', 'blacklisted', 'failed')
AND COALESCE(item.result_payload_json->>'controller_processed', 'false') <> 'true'
ORDER BY COALESCE(item.finished_at, item.updated_at) ASC, item.id ASC
LIMIT %s
FOR UPDATE SKIP LOCKED
""",
(normalized_job_id, normalized_job_id, safe_limit),
)
rows = cur.fetchall()
for row in rows:
item_id = int(row[0])
job_id = int(row[1])
domain_id = int(row[2])
current_step_code = str(row[3] or "").strip()
item_status = str(row[4] or "").strip()
attempt_count = int(row[5] or 0)
result_payload = _decode_payload(row[6])
task_mode = str(row[7] or "").strip()
domain_snapshot = _load_domain_pipeline_snapshot(cur, domain_id)
if not domain_snapshot:
cur.execute(
"""
UPDATE detect_job_items
SET result_payload_json = jsonb_set(COALESCE(result_payload_json, '{}'::jsonb), '{controller_processed}', 'true'::jsonb, true),
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""",
(item_id,),
)
summary["processed_items"] += 1
summary["skipped_items"] += 1
continue
if task_mode == "single_step":
result_field_name = str(
result_payload.get("field_name") or _PIPELINE_STEP_FIELDS.get(current_step_code) or ""
).strip()
persisted_payload = {
key: value
for key, value in dict(result_payload or {}).items()
if key not in {"controller_processed", "controller_action", "controller_processed_at"}
}
action = "single_step_ignored"
if result_field_name:
persisted_payload.setdefault("step_code", current_step_code)
persisted_payload.setdefault("step_name", _step_label(current_step_code))
persisted_payload.setdefault("field_name", result_field_name)
persisted_payload.setdefault("task_mode", "single_step")
persisted_payload.setdefault("domain_id", domain_id)
_upsert_domain_detection_field(cur, domain_id, result_field_name, persisted_payload)
action = "single_step_persisted"
processed_payload = {
**dict(result_payload or {}),
"controller_processed": True,
"controller_action": action,
"controller_processed_at": datetime.now().isoformat(timespec="seconds"),
}
cur.execute(
"""
UPDATE detect_job_items
SET result_payload_json = %s::jsonb,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""",
(json.dumps(processed_payload, ensure_ascii=False), item_id),
)
touched_job_ids.add(job_id)
summary["processed_items"] += 1
if action == "single_step_persisted":
summary["advanced_items"] += 1
else:
summary["skipped_items"] += 1
continue
result_message = str(result_payload.get("message") or "").strip()
outcome = _classify_pipeline_item_outcome(
item_status=item_status,
result_payload=result_payload,
step_code=current_step_code,
attempt_count=attempt_count,
)
action = str(outcome.get("action") or "pass").strip()
result_state = str(outcome.get("result_state") or "").strip().lower()
retry_limit = int(outcome.get("retry_limit", 0) or 0)
if action == "retry":
retry_payload = {
**result_payload,
"controller_action": "retry",
"controller_processed": False,
"controller_retry_count": int(attempt_count),
"controller_reason_code": str(outcome.get("reason_code") or "external_retry"),
}
cur.execute(
"""
UPDATE detect_job_items
SET status = 'pending',
claimed_by = '',
claim_token = '',
lease_expires_at = NULL,
finished_at = NULL,
last_error = %s,
result_payload_json = %s::jsonb,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""",
(
result_message or f"{_step_label(current_step_code)} 执行失败,等待重试",
json.dumps(retry_payload, ensure_ascii=False),
item_id,
),
)
_append_detect_job_event_with_cursor(
cur,
job_id,
event_type="step_retry_scheduled",
message=f"{domain_snapshot['domain']}{_step_label(current_step_code)} 已重投",
payload={
"domain_id": domain_id,
"domain": domain_snapshot["domain"],
"step_code": current_step_code,
"attempt_count": attempt_count,
"retry_limit": retry_limit,
"result_state": result_state,
},
)
touched_job_ids.add(job_id)
summary["processed_items"] += 1
summary["retried_items"] += 1
summary["retry_reset_items"] += 1
continue
if action == "pass":
next_step_code = resolve_domain_pipeline_step(
domain_snapshot,
settings_payload=settings_payload,
after_step_code=current_step_code,
)
if next_step_code:
next_payload = _build_step_payload(
step_code=next_step_code,
domain_snapshot=domain_snapshot,
settings_payload=settings_payload,
)
cur.execute(
"""
INSERT INTO detect_job_items (job_id, domain_id, step_code, status, step_payload_json)
VALUES (%s, %s, %s, 'pending', %s::jsonb)
ON CONFLICT (job_id, domain_id, step_code) DO NOTHING
RETURNING id
""",
(
job_id,
domain_id,
next_step_code,
json.dumps(next_payload, ensure_ascii=False),
),
)
created_next = bool(cur.fetchone())
action = "pass"
summary["advanced_items"] += 1
if created_next:
summary["created_next_items"] += 1
_append_detect_job_event_with_cursor(
cur,
job_id,
event_type="pipeline_step_advanced",
message=f"{domain_snapshot['domain']} 进入下一步骤: {_step_label(next_step_code)}",
payload={
"domain_id": domain_id,
"domain": domain_snapshot["domain"],
"current_step_code": current_step_code,
"next_step_code": next_step_code,
},
)
else:
action = "pipeline_completed"
_append_detect_job_event_with_cursor(
cur,
job_id,
event_type="pipeline_completed",
message=f"{domain_snapshot['domain']} 本次 pipeline 已完成",
payload={
"domain_id": domain_id,
"domain": domain_snapshot["domain"],
"current_step_code": current_step_code,
},
)
elif action == "black_hit":
_append_detect_job_event_with_cursor(
cur,
job_id,
event_type="pipeline_black_hit",
message=f"{domain_snapshot['domain']}{_step_label(current_step_code)} 命中黑名单并终止后续步骤",
level="warning",
payload={
"domain_id": domain_id,
"domain": domain_snapshot["domain"],
"current_step_code": current_step_code,
"result_state": result_state or "blacklisted",
},
)
elif action == "reject":
_append_detect_job_event_with_cursor(
cur,
job_id,
event_type="pipeline_rejected",
message=f"{domain_snapshot['domain']}{_step_label(current_step_code)} 被判定终止",
level="warning",
payload={
"domain_id": domain_id,
"domain": domain_snapshot["domain"],
"current_step_code": current_step_code,
"result_state": result_state or "failed",
"attempt_count": attempt_count,
"retry_limit": retry_limit,
"reason_code": str(outcome.get("reason_code") or ""),
},
)
processed_payload = {
**result_payload,
"controller_processed": True,
"controller_action": action,
"controller_reason_code": str(outcome.get("reason_code") or ""),
"controller_processed_at": datetime.now().isoformat(timespec="seconds"),
}
if action == "pass":
processed_payload["controller_result_state"] = result_state or "passed"
cur.execute(
"""
UPDATE detect_job_items
SET result_payload_json = %s::jsonb,
updated_at = CURRENT_TIMESTAMP
WHERE id = %s
""",
(json.dumps(processed_payload, ensure_ascii=False), item_id),
)
touched_job_ids.add(job_id)
summary["processed_items"] += 1
if action in {"black_hit", "reject", "pipeline_completed"}:
summary["terminal_items"] += 1
for job_id in touched_job_ids:
_refresh_detect_job_status_with_cursor(cur, job_id)
summary["job_ids"].append(job_id)
conn.commit()
summary["job_ids"] = sorted(set(int(item) for item in summary["job_ids"]))
return summary
def process_detect_pipeline_now(limit: int = 2000, job_id: int | None = None) -> tuple[bool, str, dict]:
attempts = 3
delay_seconds = 0.05
for attempt in range(1, attempts + 1):
try:
data = process_detect_pipeline(limit=limit, job_id=job_id)
processed = int(data.get("processed_items", 0) or 0)
advanced = int(data.get("advanced_items", 0) or 0)
retried = int(data.get("retried_items", 0) or 0)
message = f"pipeline 处理完成processed={processed}, advanced={advanced}, retried={retried}"
if attempt > 1:
message = f"{message}deadlock 自动重试 {attempt - 1} 次后成功)"
return True, message, {"action": "process_pipeline", "retry_attempts": attempt - 1, **data}
except Exception as exc:
if not is_retryable_db_error(exc) or attempt >= attempts:
raise
time.sleep(delay_seconds)
delay_seconds *= 2
data = process_detect_pipeline(limit=limit, job_id=job_id)
processed = int(data.get("processed_items", 0) or 0)
advanced = int(data.get("advanced_items", 0) or 0)
retried = int(data.get("retried_items", 0) or 0)
message = f"pipeline 处理完成processed={processed}, advanced={advanced}, retried={retried}"
return True, message, {"action": "process_pipeline", **data}
def _append_detect_job_event_with_cursor(
cur,
job_id: int,
*,
event_type: str,
message: str,
level: str = "info",
payload: dict | None = None,
node_code: str | None = None,
) -> bool:
cur.execute(
"SELECT 1 FROM detect_jobs WHERE id = %s LIMIT 1",
(int(job_id),),
)
if cur.fetchone() is None:
return False
cur.execute(
"""
INSERT INTO detect_run_events (job_id, node_code, event_type, level, message, payload_json)
VALUES (%s, %s, %s, %s, %s, %s::jsonb)
""",
(
int(job_id),
node_code or settings.node_code,
str(event_type or "").strip() or "info",
str(level or "info").strip() or "info",
str(message or "").strip(),
json.dumps(payload or {}, ensure_ascii=False),
),
)
return True
def append_detect_job_event(
job_id: int,
*,
event_type: str,
message: str,
level: str = "info",
payload: dict | None = None,
node_code: str | None = None,
) -> None:
inserted = False
with get_db() as conn:
with conn.cursor() as cur:
inserted = _append_detect_job_event_with_cursor(
cur,
job_id,
event_type=event_type,
message=message,
level=level,
payload=payload,
node_code=node_code,
)
if inserted:
conn.commit()
try:
push_debug_event(
service="detect-job",
event_type=str(event_type or "").strip() or "info",
level=str(level or "info").strip() or "info",
message=str(message or "").strip(),
payload={
"job_id": int(job_id),
"node_code": node_code or settings.node_code,
**(payload or {}),
},
)
except Exception:
pass
def create_detect_job_if_needed(limit: int = 1000, created_by: str = "system", step_code: str | None = None) -> dict | None:
job_definition = resolve_detect_job_definition(step_code)
existing = get_active_detect_job_summary()
if existing:
existing_task_mode = str(existing.get("task_mode") or "").strip()
existing_step_code = normalize_detect_step_code(existing.get("step_code"))
if (
existing_task_mode == str(job_definition["task_mode"]).strip()
and existing_step_code == normalize_detect_step_code(job_definition["step_code"])
):
return existing
settings_payload = get_settings_payload()
with get_db() as conn:
conn.autocommit = False
with conn.cursor() as cur:
cur.execute(_selection_sql(), (max(1, int(limit or 1000)),))
domain_ids = [row[0] for row in cur.fetchall()]
if not domain_ids:
conn.rollback()
if not domain_ids:
try:
from app.services.sync_push_service import pull_detect_task_batch_now
pull_detect_task_batch_now(limit=max(1, int(limit or 1000)))
except Exception:
pass
with get_db() as conn:
conn.autocommit = False
with conn.cursor() as cur:
cur.execute(_selection_sql(), (max(1, int(limit or 1000)),))
domain_ids = [row[0] for row in cur.fetchall()]
if not domain_ids:
conn.rollback()
return None
with get_db() as conn:
conn.autocommit = False
with conn.cursor() as cur:
domain_snapshots: list[dict] = []
for domain_id in domain_ids:
snapshot = _load_domain_pipeline_snapshot(cur, int(domain_id))
if snapshot:
domain_snapshots.append(snapshot)
if not domain_snapshots:
conn.rollback()
return None
job_prefix = "step" if job_definition["is_single_step"] else "detect"
job_code = f"{job_prefix}-{datetime.now().strftime('%Y%m%d%H%M%S')}-{uuid4().hex[:6]}"
plan_hash = uuid4().hex
cur.execute(
"""
INSERT INTO detect_jobs (job_code, source, plan_hash, task_mode, step_code, status, remark, created_by)
VALUES (%s, %s, %s, %s, %s, 'pending', %s, %s)
RETURNING id
""",
(
job_code,
job_definition["source"],
plan_hash,
job_definition["task_mode"],
job_definition["step_code"],
f"API 创建{job_definition['label']}任务,待检测域名 {len(domain_ids)}",
created_by,
),
)
job_id = cur.fetchone()[0]
queued_count = 0
skipped_count = 0
for snapshot in domain_snapshots:
item_step_code = (
job_definition["step_code"]
if job_definition["is_single_step"]
else resolve_initial_domain_pipeline_item(snapshot, settings_payload=settings_payload)[0]
)
if not item_step_code:
skipped_count += 1
continue
_, step_payload = resolve_initial_domain_pipeline_item(snapshot, settings_payload=settings_payload)
cur.execute(
"""
INSERT INTO detect_job_items (job_id, domain_id, step_code, status, step_payload_json)
VALUES (%s, %s, %s, 'pending', %s::jsonb)
ON CONFLICT (job_id, domain_id, step_code) DO NOTHING
RETURNING id
""",
(
job_id,
int(snapshot["id"]),
item_step_code,
json.dumps(step_payload, ensure_ascii=False),
),
)
if cur.fetchone():
queued_count += 1
else:
skipped_count += 1
if queued_count <= 0:
conn.rollback()
return None
cur.execute(
"""
INSERT INTO detect_run_events (job_id, node_code, event_type, level, message, payload_json)
VALUES (%s, %s, %s, %s, %s, %s::jsonb)
""",
(
job_id,
settings.node_code,
"job_created",
"info",
f"创建检测任务 {job_code},已入队 {queued_count} 个任务项",
json.dumps(
{
"count": len(domain_ids),
"queued_count": queued_count,
"skipped_count": skipped_count,
"task_mode": job_definition["task_mode"],
"step_code": job_definition["step_code"],
},
ensure_ascii=False,
),
),
)
conn.commit()
return get_detect_job_summary(job_id)