feat: ingest mainland detect result events
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import socket
|
||||
import urllib.error
|
||||
@@ -14,6 +15,14 @@ from app.services.cluster_runtime_service import cleanup_imported_runtime_nodes,
|
||||
from app.services.sync_record_service import _decode_json, _normalize_region
|
||||
|
||||
|
||||
_DETECT_RESULT_EVENT_TYPES = {
|
||||
"domain_started",
|
||||
"domain_completed",
|
||||
"domain_failed",
|
||||
"domain_blacklisted",
|
||||
}
|
||||
|
||||
|
||||
def _format_time(value: datetime | None) -> str:
|
||||
return value.isoformat(sep=" ", timespec="seconds") if value else ""
|
||||
|
||||
@@ -664,6 +673,173 @@ def _update_push_attempt(record_id: int, *, status: str, payload: dict | None =
|
||||
conn.commit()
|
||||
|
||||
|
||||
def _build_detect_result_import_fingerprint(
|
||||
*,
|
||||
source_region: str,
|
||||
source_record_id: int,
|
||||
source_job_code: str,
|
||||
event: dict,
|
||||
) -> str:
|
||||
payload = _decode_json(event.get("payload"))
|
||||
seed = {
|
||||
"source_region": str(source_region or "").strip(),
|
||||
"source_record_id": int(source_record_id or 0),
|
||||
"source_job_code": str(source_job_code or "").strip(),
|
||||
"node_code": str(event.get("node_code") or "").strip(),
|
||||
"event_type": str(event.get("event_type") or "").strip(),
|
||||
"message": str(event.get("message") or "").strip(),
|
||||
"created_at": str(event.get("created_at") or "").strip(),
|
||||
"domain": str(payload.get("domain") or "").strip(),
|
||||
"domain_id": str(payload.get("domain_id") or "").strip(),
|
||||
"status": str(payload.get("status") or "").strip(),
|
||||
"cycle_token": str(payload.get("cycle_token") or "").strip(),
|
||||
}
|
||||
return hashlib.sha1(json.dumps(seed, ensure_ascii=False, sort_keys=True).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _extract_detect_result_projection_events(
|
||||
*,
|
||||
source_region: str,
|
||||
source_record_id: int,
|
||||
projection: dict,
|
||||
) -> list[dict]:
|
||||
source_job = projection.get("job") or {}
|
||||
source_job_code = str(source_job.get("job_code") or "").strip()
|
||||
source_job_id = int(source_job.get("job_id") or 0)
|
||||
events: list[dict] = []
|
||||
for raw_event in list(projection.get("recent_domain_events") or []):
|
||||
event_type = str(raw_event.get("event_type") or "").strip()
|
||||
if event_type not in _DETECT_RESULT_EVENT_TYPES:
|
||||
continue
|
||||
payload = _decode_json(raw_event.get("payload"))
|
||||
fingerprint = _build_detect_result_import_fingerprint(
|
||||
source_region=source_region,
|
||||
source_record_id=source_record_id,
|
||||
source_job_code=source_job_code,
|
||||
event=raw_event,
|
||||
)
|
||||
payload.update(
|
||||
{
|
||||
"imported_from_projection": True,
|
||||
"import_source_region": source_region,
|
||||
"import_source_record_id": int(source_record_id or 0),
|
||||
"import_source_job_id": source_job_id,
|
||||
"import_source_job_code": source_job_code,
|
||||
"import_fingerprint": fingerprint,
|
||||
}
|
||||
)
|
||||
events.append(
|
||||
{
|
||||
"node_code": str(raw_event.get("node_code") or "").strip(),
|
||||
"event_type": event_type,
|
||||
"level": str(raw_event.get("level") or "info").strip() or "info",
|
||||
"message": str(raw_event.get("message") or "").strip(),
|
||||
"created_at": str(raw_event.get("created_at") or "").strip(),
|
||||
"payload": payload,
|
||||
"fingerprint": fingerprint,
|
||||
}
|
||||
)
|
||||
return events
|
||||
|
||||
|
||||
def _resolve_detect_result_target_job_id() -> int:
|
||||
from app.services.detect_job_service import get_active_detect_job_summary
|
||||
|
||||
active_job = get_active_detect_job_summary(event_limit=1) or {}
|
||||
return int(active_job.get("job_id") or 0)
|
||||
|
||||
|
||||
def _parse_event_created_at(value: str) -> datetime | None:
|
||||
text = str(value or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
try:
|
||||
return datetime.fromisoformat(text)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _import_detect_result_projection_events(
|
||||
*,
|
||||
source_region: str,
|
||||
source_record_id: int,
|
||||
projection: dict,
|
||||
) -> dict:
|
||||
events = _extract_detect_result_projection_events(
|
||||
source_region=source_region,
|
||||
source_record_id=source_record_id,
|
||||
projection=projection,
|
||||
)
|
||||
if not events:
|
||||
return {"imported_count": 0, "deduplicated_count": 0, "target_job_id": 0}
|
||||
|
||||
target_job_id = _resolve_detect_result_target_job_id()
|
||||
if target_job_id <= 0:
|
||||
return {"imported_count": 0, "deduplicated_count": 0, "target_job_id": 0}
|
||||
|
||||
imported_count = 0
|
||||
deduplicated_count = 0
|
||||
with get_db() as conn:
|
||||
with conn.cursor() as cur:
|
||||
for event in events:
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id
|
||||
FROM detect_run_events
|
||||
WHERE job_id = %s
|
||||
AND (payload_json->>'import_fingerprint') = %s
|
||||
ORDER BY id DESC
|
||||
LIMIT 1
|
||||
""",
|
||||
(target_job_id, event["fingerprint"]),
|
||||
)
|
||||
if cur.fetchone():
|
||||
deduplicated_count += 1
|
||||
continue
|
||||
|
||||
created_at = _parse_event_created_at(event.get("created_at", ""))
|
||||
if created_at:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO detect_run_events (
|
||||
job_id, node_code, event_type, level, message, payload_json, created_at
|
||||
) VALUES (%s, %s, %s, %s, %s, %s::jsonb, %s)
|
||||
""",
|
||||
(
|
||||
target_job_id,
|
||||
event["node_code"],
|
||||
event["event_type"],
|
||||
event["level"],
|
||||
event["message"],
|
||||
json.dumps(event["payload"], ensure_ascii=False),
|
||||
created_at,
|
||||
),
|
||||
)
|
||||
else:
|
||||
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)
|
||||
""",
|
||||
(
|
||||
target_job_id,
|
||||
event["node_code"],
|
||||
event["event_type"],
|
||||
event["level"],
|
||||
event["message"],
|
||||
json.dumps(event["payload"], ensure_ascii=False),
|
||||
),
|
||||
)
|
||||
imported_count += 1
|
||||
conn.commit()
|
||||
return {
|
||||
"imported_count": imported_count,
|
||||
"deduplicated_count": deduplicated_count,
|
||||
"target_job_id": target_job_id,
|
||||
}
|
||||
|
||||
|
||||
def ingest_runtime_projection(payload: dict, *, shared_token: str | None = None) -> tuple[bool, str, dict]:
|
||||
configured_token = str(settings.sync_shared_token or "").strip()
|
||||
incoming_token = str(shared_token or "").strip()
|
||||
@@ -699,6 +875,14 @@ def ingest_runtime_projection(payload: dict, *, shared_token: str | None = None)
|
||||
if existing:
|
||||
if sync_type == "runtime_projection":
|
||||
_refresh_remote_runtime_node(source_region=source_region, projection=projection, received_at=received_at)
|
||||
if sync_type == "detect_result_projection":
|
||||
import_result = _import_detect_result_projection_events(
|
||||
source_region=source_region,
|
||||
source_record_id=source_record_id,
|
||||
projection=projection,
|
||||
)
|
||||
else:
|
||||
import_result = {}
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE detect_sync_records
|
||||
@@ -708,7 +892,11 @@ def ingest_runtime_projection(payload: dict, *, shared_token: str | None = None)
|
||||
(int(existing[0]),),
|
||||
)
|
||||
conn.commit()
|
||||
return True, "同步投影已存在,已按幂等处理", {"record_id": int(existing[0]), "deduplicated": True}
|
||||
return True, "同步投影已存在,已按幂等处理", {
|
||||
"record_id": int(existing[0]),
|
||||
"deduplicated": True,
|
||||
"event_import": import_result,
|
||||
}
|
||||
|
||||
stored_payload = {
|
||||
"sync_type": sync_type,
|
||||
@@ -737,7 +925,19 @@ def ingest_runtime_projection(payload: dict, *, shared_token: str | None = None)
|
||||
conn.commit()
|
||||
if sync_type == "runtime_projection":
|
||||
_refresh_remote_runtime_node(source_region=source_region, projection=projection, received_at=received_at)
|
||||
return True, "同步投影接收成功", {"record_id": record_id, "deduplicated": False}
|
||||
if sync_type == "detect_result_projection":
|
||||
import_result = _import_detect_result_projection_events(
|
||||
source_region=source_region,
|
||||
source_record_id=source_record_id,
|
||||
projection=projection,
|
||||
)
|
||||
else:
|
||||
import_result = {}
|
||||
return True, "同步投影接收成功", {
|
||||
"record_id": record_id,
|
||||
"deduplicated": False,
|
||||
"event_import": import_result,
|
||||
}
|
||||
|
||||
|
||||
def _push_projection_now(sync_type: str, ingest_url: str) -> tuple[bool, str, dict]:
|
||||
|
||||
Reference in New Issue
Block a user