feat: stabilize multi-region runtime sync and worker orchestration
This commit is contained in:
File diff suppressed because it is too large
Load Diff
194
domainCheck/app/utils/detection_results.py
Normal file
194
domainCheck/app/utils/detection_results.py
Normal file
@@ -0,0 +1,194 @@
|
||||
import json
|
||||
|
||||
|
||||
def build_detection_result(
|
||||
*,
|
||||
status=None,
|
||||
state=None,
|
||||
message="",
|
||||
error=None,
|
||||
**payload,
|
||||
):
|
||||
result = dict(payload)
|
||||
result["status"] = bool(status) if status is not None else None
|
||||
result["message"] = message or ""
|
||||
|
||||
if error:
|
||||
result["state"] = "error"
|
||||
result["error"] = str(error)
|
||||
if not result["message"]:
|
||||
result["message"] = str(error)
|
||||
else:
|
||||
result["state"] = state or _default_state_for_status(status)
|
||||
result.pop("error", None)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _default_state_for_status(status):
|
||||
if status is True:
|
||||
return "positive"
|
||||
if status is False:
|
||||
return "negative"
|
||||
return "ok"
|
||||
|
||||
|
||||
def load_detection_result(value):
|
||||
if value is None:
|
||||
return {}
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
decoded = json.loads(value)
|
||||
except Exception:
|
||||
return {}
|
||||
return decoded if isinstance(decoded, dict) else {}
|
||||
return {}
|
||||
|
||||
|
||||
def resolve_detection_status(value, *legacy_keys):
|
||||
data = load_detection_result(value)
|
||||
if not data:
|
||||
return False
|
||||
|
||||
status = data.get("status")
|
||||
if status is not None:
|
||||
return bool(status)
|
||||
|
||||
for key in legacy_keys:
|
||||
if data.get(key) is not None:
|
||||
return bool(data.get(key))
|
||||
return False
|
||||
|
||||
|
||||
def build_manual_detection_result(status, *, legacy_key=None, message="人工更新"):
|
||||
payload = {}
|
||||
if legacy_key:
|
||||
payload[legacy_key] = bool(status)
|
||||
return build_detection_result(
|
||||
status=bool(status),
|
||||
state="manual",
|
||||
message=message,
|
||||
**payload,
|
||||
)
|
||||
|
||||
|
||||
def normalize_detector_result(name, result):
|
||||
result = load_detection_result(result)
|
||||
if result.get("error"):
|
||||
return build_detection_result(error=result.get("error"), **_without_meta(result))
|
||||
|
||||
if name == "baidu_history":
|
||||
has_history = bool(result.get("has_history"))
|
||||
has_gray = bool(result.get("has_gray"))
|
||||
return build_detection_result(
|
||||
status=has_history,
|
||||
state="risk" if has_gray else None,
|
||||
has_history=has_history,
|
||||
has_gray=has_gray,
|
||||
)
|
||||
|
||||
if name in {"baidu_site", "qihu360_site", "google_site"}:
|
||||
has_index = bool(result.get("has_收录"))
|
||||
normalized = build_detection_result(
|
||||
status=has_index,
|
||||
has_收录=has_index,
|
||||
subdomains=list(result.get("subdomains", []) or []),
|
||||
)
|
||||
return normalized
|
||||
|
||||
if name == "chinaz_info":
|
||||
return build_detection_result(
|
||||
status=None,
|
||||
title=result.get("title", ""),
|
||||
category=result.get("category", ""),
|
||||
has_sensitive=bool(result.get("has_sensitive")),
|
||||
)
|
||||
|
||||
if name == "aizhan_info":
|
||||
return build_detection_result(
|
||||
status=None,
|
||||
title=result.get("title", ""),
|
||||
risk=result.get("risk", ""),
|
||||
has_sensitive=bool(result.get("has_sensitive")),
|
||||
)
|
||||
|
||||
if name == "juziseo_info":
|
||||
history = normalize_detector_result("juziseo_history", result.get("history"))
|
||||
backlink = normalize_detector_result("juziseo_backlink", result.get("backlink"))
|
||||
nested_error = history.get("error") or backlink.get("error")
|
||||
return build_detection_result(
|
||||
status=None,
|
||||
error=nested_error,
|
||||
history=history,
|
||||
backlink=backlink,
|
||||
)
|
||||
|
||||
if name == "juziseo_history":
|
||||
return build_detection_result(
|
||||
status=None,
|
||||
state="risk" if result.get("has_sensitive") or result.get("has_subdomains") else None,
|
||||
has_sensitive=bool(result.get("has_sensitive")),
|
||||
has_baidu_history=bool(result.get("has_baidu_history")),
|
||||
has_subdomains=bool(result.get("has_subdomains")),
|
||||
is_simplified=bool(result.get("is_simplified", True)),
|
||||
)
|
||||
|
||||
if name == "juziseo_backlink":
|
||||
return build_detection_result(
|
||||
status=None,
|
||||
state="risk" if result.get("has_sensitive") or result.get("has_subdomains") else None,
|
||||
has_sensitive=bool(result.get("has_sensitive")),
|
||||
has_subdomains=bool(result.get("has_subdomains")),
|
||||
)
|
||||
|
||||
if name == "jucha_info":
|
||||
whois = normalize_detector_result("jucha_whois", result.get("whois"))
|
||||
beian = normalize_detector_result("jucha_beian", result.get("beian"))
|
||||
intercept = normalize_detector_result("jucha_intercept", result.get("intercept"))
|
||||
nested_error = whois.get("error") or beian.get("error") or intercept.get("error")
|
||||
return build_detection_result(
|
||||
status=None,
|
||||
error=nested_error,
|
||||
whois=whois,
|
||||
beian=beian,
|
||||
intercept=intercept,
|
||||
)
|
||||
|
||||
if name == "jucha_whois":
|
||||
whois_status = result.get("status", "")
|
||||
hold = whois_status in {"clientHold", "serverHold"}
|
||||
return build_detection_result(
|
||||
status=None,
|
||||
state="risk" if hold else None,
|
||||
whois_status=whois_status,
|
||||
)
|
||||
|
||||
if name == "jucha_beian":
|
||||
has_beian = bool(result.get("has_beian"))
|
||||
return build_detection_result(
|
||||
status=has_beian,
|
||||
has_beian=has_beian,
|
||||
beian_year=result.get("beian_year", ""),
|
||||
is_enterprise=bool(result.get("is_enterprise")),
|
||||
beian_match=bool(result.get("beian_match")),
|
||||
)
|
||||
|
||||
if name == "jucha_intercept":
|
||||
normal = bool(result.get("normal"))
|
||||
return build_detection_result(
|
||||
status=normal,
|
||||
state="risk" if not normal else None,
|
||||
normal=normal,
|
||||
)
|
||||
|
||||
return build_detection_result(status=None, **result)
|
||||
|
||||
|
||||
def _without_meta(result):
|
||||
return {
|
||||
key: value
|
||||
for key, value in result.items()
|
||||
if key not in {"status", "state", "message", "error"}
|
||||
}
|
||||
129
domainCheck/app/utils/redis_client.py
Normal file
129
domainCheck/app/utils/redis_client.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
|
||||
import redis
|
||||
|
||||
from app.config import config
|
||||
|
||||
|
||||
_CLIENTS: dict[tuple[str, bool], redis.Redis] = {}
|
||||
_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _safe_int(raw_value: object, default: int, minimum: int) -> int:
|
||||
try:
|
||||
parsed = int(raw_value)
|
||||
except Exception:
|
||||
parsed = default
|
||||
return max(minimum, parsed)
|
||||
|
||||
|
||||
def _safe_float(raw_value: object, default: float, minimum: float) -> float:
|
||||
try:
|
||||
parsed = float(raw_value)
|
||||
except Exception:
|
||||
parsed = default
|
||||
return max(minimum, parsed)
|
||||
|
||||
|
||||
def _pool_options(role: str, *, decode_responses: bool) -> dict:
|
||||
normalized_role = str(role or "standard").strip().lower() or "standard"
|
||||
node_code = str(getattr(config, "NODE_CODE", "") or "").strip() or "unknown"
|
||||
|
||||
if normalized_role == "pubsub":
|
||||
return {
|
||||
"host": config.REDIS_HOST,
|
||||
"port": config.REDIS_PORT,
|
||||
"password": config.REDIS_PASSWORD or None,
|
||||
"db": config.REDIS_DB,
|
||||
"decode_responses": decode_responses,
|
||||
"socket_connect_timeout": _safe_float(
|
||||
os.getenv("DOMAINCHECK_REDIS_PUBSUB_CONNECT_TIMEOUT", "30"),
|
||||
30.0,
|
||||
1.0,
|
||||
),
|
||||
"socket_timeout": _safe_float(
|
||||
os.getenv("DOMAINCHECK_REDIS_PUBSUB_SOCKET_TIMEOUT", "60"),
|
||||
60.0,
|
||||
1.0,
|
||||
),
|
||||
"health_check_interval": _safe_int(
|
||||
os.getenv("DOMAINCHECK_REDIS_HEALTH_CHECK_INTERVAL", "30"),
|
||||
30,
|
||||
0,
|
||||
),
|
||||
"retry_on_timeout": True,
|
||||
"max_connections": _safe_int(
|
||||
os.getenv("DOMAINCHECK_REDIS_PUBSUB_MAX_CONNECTIONS", "2"),
|
||||
2,
|
||||
1,
|
||||
),
|
||||
"timeout": _safe_float(
|
||||
os.getenv("DOMAINCHECK_REDIS_PUBSUB_POOL_TIMEOUT", "5"),
|
||||
5.0,
|
||||
0.1,
|
||||
),
|
||||
"client_name": f"domaincheck:pubsub:{node_code}:{os.getpid()}",
|
||||
}
|
||||
|
||||
return {
|
||||
"host": config.REDIS_HOST,
|
||||
"port": config.REDIS_PORT,
|
||||
"password": config.REDIS_PASSWORD or None,
|
||||
"db": config.REDIS_DB,
|
||||
"decode_responses": decode_responses,
|
||||
"socket_connect_timeout": _safe_float(
|
||||
os.getenv("DOMAINCHECK_REDIS_CONNECT_TIMEOUT", "5"),
|
||||
5.0,
|
||||
0.5,
|
||||
),
|
||||
"socket_timeout": _safe_float(
|
||||
os.getenv("DOMAINCHECK_REDIS_SOCKET_TIMEOUT", "10"),
|
||||
10.0,
|
||||
0.5,
|
||||
),
|
||||
"health_check_interval": _safe_int(
|
||||
os.getenv("DOMAINCHECK_REDIS_HEALTH_CHECK_INTERVAL", "30"),
|
||||
30,
|
||||
0,
|
||||
),
|
||||
"retry_on_timeout": True,
|
||||
"max_connections": _safe_int(
|
||||
os.getenv("DOMAINCHECK_REDIS_MAX_CONNECTIONS", "12"),
|
||||
12,
|
||||
1,
|
||||
),
|
||||
"timeout": _safe_float(
|
||||
os.getenv("DOMAINCHECK_REDIS_POOL_TIMEOUT", "1.5"),
|
||||
1.5,
|
||||
0.1,
|
||||
),
|
||||
"client_name": f"domaincheck:standard:{node_code}:{os.getpid()}",
|
||||
}
|
||||
|
||||
|
||||
def get_redis_client(*, role: str = "standard", decode_responses: bool = True) -> redis.Redis:
|
||||
normalized_role = str(role or "standard").strip().lower() or "standard"
|
||||
cache_key = (normalized_role, bool(decode_responses))
|
||||
with _LOCK:
|
||||
cached = _CLIENTS.get(cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
pool = redis.BlockingConnectionPool(**_pool_options(normalized_role, decode_responses=decode_responses))
|
||||
client = redis.Redis(connection_pool=pool)
|
||||
_CLIENTS[cache_key] = client
|
||||
return client
|
||||
|
||||
|
||||
def reset_redis_clients_for_tests() -> None:
|
||||
with _LOCK:
|
||||
clients = list(_CLIENTS.values())
|
||||
_CLIENTS.clear()
|
||||
for client in clients:
|
||||
try:
|
||||
client.close()
|
||||
except Exception:
|
||||
pass
|
||||
Reference in New Issue
Block a user