d
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import traceback
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from datetime import datetime
|
||||
from urllib.parse import urlparse
|
||||
from uuid import uuid4
|
||||
|
||||
from app.services.ops_action_executor_core import (
|
||||
@@ -28,6 +31,44 @@ NODE_CODE = str(os.getenv("NODE_CODE", "")).strip()
|
||||
NODE_REGION = str(os.getenv("NODE_REGION", "mainland")).strip() or "mainland"
|
||||
NODE_ROLE = str(os.getenv("NODE_ROLE", "worker")).strip() or "worker"
|
||||
AGENT_POLL_INTERVAL_SECONDS = max(2, int(os.getenv("OPS_AGENT_POLL_INTERVAL_SECONDS", "5") or 5))
|
||||
AGENT_RUNTIME_CONFIG_SYNC_INTERVAL_SECONDS = max(
|
||||
10,
|
||||
int(os.getenv("OPS_AGENT_RUNTIME_CONFIG_SYNC_INTERVAL_SECONDS", "30") or 30),
|
||||
)
|
||||
AGENT_HTTP_TIMEOUT_SECONDS = max(10, int(os.getenv("OPS_AGENT_HTTP_TIMEOUT_SECONDS", "60") or 60))
|
||||
AGENT_REGISTER_TIMEOUT_SECONDS = max(
|
||||
10,
|
||||
int(os.getenv("OPS_AGENT_REGISTER_TIMEOUT_SECONDS", str(AGENT_HTTP_TIMEOUT_SECONDS)) or AGENT_HTTP_TIMEOUT_SECONDS),
|
||||
)
|
||||
AGENT_HEARTBEAT_TIMEOUT_SECONDS = max(
|
||||
10,
|
||||
int(os.getenv("OPS_AGENT_HEARTBEAT_TIMEOUT_SECONDS", str(AGENT_HTTP_TIMEOUT_SECONDS)) or AGENT_HTTP_TIMEOUT_SECONDS),
|
||||
)
|
||||
AGENT_PULL_TIMEOUT_SECONDS = max(
|
||||
10,
|
||||
int(os.getenv("OPS_AGENT_PULL_TIMEOUT_SECONDS", str(AGENT_HTTP_TIMEOUT_SECONDS)) or AGENT_HTTP_TIMEOUT_SECONDS),
|
||||
)
|
||||
AGENT_RUNTIME_CONFIG_TIMEOUT_SECONDS = max(
|
||||
10,
|
||||
int(
|
||||
os.getenv("OPS_AGENT_RUNTIME_CONFIG_TIMEOUT_SECONDS", str(AGENT_HTTP_TIMEOUT_SECONDS))
|
||||
or AGENT_HTTP_TIMEOUT_SECONDS
|
||||
),
|
||||
)
|
||||
AGENT_JOB_COMPLETE_TIMEOUT_SECONDS = max(
|
||||
10,
|
||||
int(
|
||||
os.getenv("OPS_AGENT_JOB_COMPLETE_TIMEOUT_SECONDS", str(AGENT_HTTP_TIMEOUT_SECONDS))
|
||||
or AGENT_HTTP_TIMEOUT_SECONDS
|
||||
),
|
||||
)
|
||||
AGENT_JOB_EVENT_TIMEOUT_SECONDS = max(
|
||||
10,
|
||||
int(
|
||||
os.getenv("OPS_AGENT_JOB_EVENT_TIMEOUT_SECONDS", str(max(15, AGENT_HTTP_TIMEOUT_SECONDS // 2)))
|
||||
or max(15, AGENT_HTTP_TIMEOUT_SECONDS // 2)
|
||||
),
|
||||
)
|
||||
WORKER_SERVICE_NAME = str(os.getenv("WORKER_SERVICE_NAME", os.getenv("WORKER_SERVICE", "domaincheck-worker"))).strip() or "domaincheck-worker"
|
||||
API_SERVICE_NAME = str(os.getenv("API_SERVICE_NAME", "domaincheck-api")).strip() or "domaincheck-api"
|
||||
SYNC_AGENT_SERVICE_NAME = str(os.getenv("SYNC_AGENT_SERVICE_NAME", "domaincheck-sync-agent")).strip() or "domaincheck-sync-agent"
|
||||
@@ -50,6 +91,7 @@ _LAST_QUEUE_FLUSH_SUMMARY = {
|
||||
"dead_letter": 0,
|
||||
"last_flush_at": "",
|
||||
}
|
||||
_LAST_RUNTIME_CONFIG_HASH = ""
|
||||
|
||||
|
||||
def _normalize_text_list(raw_value: object) -> list[str]:
|
||||
@@ -98,6 +140,7 @@ AGENT_CAPABILITIES = _json_env(
|
||||
"runtime.restart_api",
|
||||
"runtime.start_sync_agent",
|
||||
"runtime.stop_sync_agent",
|
||||
"runtime.reset_lab_state",
|
||||
"health.snapshot",
|
||||
"logs.collect",
|
||||
"diagnostics.collect",
|
||||
@@ -601,7 +644,7 @@ def _headers() -> dict[str, str]:
|
||||
}
|
||||
|
||||
|
||||
def _request(method: str, path: str, payload: dict | None = None, timeout: int = 30) -> dict:
|
||||
def _request(method: str, path: str, payload: dict | None = None, timeout: int = AGENT_HTTP_TIMEOUT_SECONDS) -> dict:
|
||||
if not CONTROL_PLANE_BASE_URL:
|
||||
raise RuntimeError("OPS_CONTROL_PLANE_BASE_URL 未配置")
|
||||
if not AGENT_TOKEN:
|
||||
@@ -614,22 +657,286 @@ def _request(method: str, path: str, payload: dict | None = None, timeout: int =
|
||||
return json.loads(body or "{}")
|
||||
|
||||
|
||||
def _post(path: str, payload: dict, timeout: int = 30) -> dict:
|
||||
def _post(path: str, payload: dict, timeout: int = AGENT_HTTP_TIMEOUT_SECONDS) -> dict:
|
||||
return _request("POST", path, payload, timeout=timeout)
|
||||
|
||||
|
||||
def _hostname() -> str:
|
||||
def _pull_runtime_config(timeout: int = AGENT_RUNTIME_CONFIG_TIMEOUT_SECONDS) -> dict:
|
||||
response = _post("/api/v1/ops/agent/runtime-config", {"node_code": NODE_CODE}, timeout=timeout)
|
||||
_ensure_ok_response(response, "agent runtime config pull failed")
|
||||
data = response.get("data") or {}
|
||||
return dict(data.get("bundle") or {})
|
||||
|
||||
|
||||
def _publish_local_config_update(config_type: str) -> None:
|
||||
try:
|
||||
return socket.gethostname()
|
||||
from app.core.redis_client import get_redis
|
||||
|
||||
redis_client = get_redis()
|
||||
redis_client.publish("domain_tool:config_update", str(config_type or "").strip() or "config")
|
||||
except Exception:
|
||||
return ""
|
||||
return
|
||||
|
||||
|
||||
def _apply_runtime_config(bundle: dict) -> bool:
|
||||
global _LAST_RUNTIME_CONFIG_HASH
|
||||
|
||||
normalized_bundle = dict(bundle or {})
|
||||
bundle_hash = str(normalized_bundle.get("config_hash") or "").strip()
|
||||
if not bundle_hash:
|
||||
bundle_hash = hashlib.sha256(
|
||||
json.dumps(normalized_bundle, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
||||
).hexdigest()
|
||||
if bundle_hash and bundle_hash == _LAST_RUNTIME_CONFIG_HASH:
|
||||
return False
|
||||
|
||||
from app.core.files import write_json
|
||||
from app.services.runtime_settings_service import update_runtime_settings
|
||||
from app.services.sensitive_words_service import save_sensitive_words_payload
|
||||
|
||||
detect_options = dict(normalized_bundle.get("detect_options") or {})
|
||||
proxy_config = dict(normalized_bundle.get("proxy_config") or {})
|
||||
thread_count = int(normalized_bundle.get("thread_count", 2) or 2)
|
||||
node_thread_counts = dict(normalized_bundle.get("node_thread_counts") or {})
|
||||
runtime_settings = dict(normalized_bundle.get("runtime_settings") or {})
|
||||
sensitive_words = dict(normalized_bundle.get("sensitive_words") or {})
|
||||
sensitive_words_text = str(sensitive_words.get("text") or "")
|
||||
sensitive_word_items = list(sensitive_words.get("items") or [])
|
||||
|
||||
write_json("detect_options.json", detect_options)
|
||||
write_json("proxy_config.json", proxy_config)
|
||||
write_json("thread_count.json", {"thread_count": str(thread_count)})
|
||||
write_json("node_thread_counts.json", node_thread_counts)
|
||||
write_json("runtime_settings.json", runtime_settings)
|
||||
write_json("runtime/runtime_settings.json", runtime_settings)
|
||||
write_json(
|
||||
"runtime/sensitive_words.json",
|
||||
{
|
||||
"items": sensitive_word_items,
|
||||
"text": sensitive_words_text,
|
||||
"total": int(sensitive_words.get("total", 0) or 0),
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
update_runtime_settings(runtime_settings)
|
||||
except Exception as exc:
|
||||
_log(f"runtime settings local api sync skipped: {exc}")
|
||||
|
||||
try:
|
||||
save_sensitive_words_payload({"text": sensitive_words_text})
|
||||
except Exception as exc:
|
||||
_log(f"sensitive words db sync skipped: {exc}")
|
||||
|
||||
try:
|
||||
from app.core.redis_client import get_redis
|
||||
|
||||
redis_client = get_redis()
|
||||
redis_client.set("domain_tool:detect_options", json.dumps(detect_options, ensure_ascii=False))
|
||||
redis_client.set("domain_tool:proxy_config", json.dumps(proxy_config, ensure_ascii=False))
|
||||
redis_client.set("domain_tool:thread_count", thread_count)
|
||||
redis_client.set("domain_tool:node_thread_counts", json.dumps(node_thread_counts, ensure_ascii=False))
|
||||
redis_client.set("domain_tool:runtime_settings", json.dumps(runtime_settings, ensure_ascii=False))
|
||||
redis_client.set("domain_tool:sensitive_words", json.dumps(sensitive_word_items, ensure_ascii=False))
|
||||
redis_client.publish("domain_tool:config_update", "detect_options")
|
||||
redis_client.publish("domain_tool:config_update", "proxy_config")
|
||||
redis_client.publish("domain_tool:config_update", "thread_count")
|
||||
redis_client.publish("domain_tool:config_update", "node_thread_counts")
|
||||
redis_client.publish("domain_tool:config_update", "runtime_settings")
|
||||
redis_client.publish("domain_tool:config_update", "sensitive_words")
|
||||
except Exception:
|
||||
_publish_local_config_update("detect_options")
|
||||
_publish_local_config_update("proxy_config")
|
||||
_publish_local_config_update("thread_count")
|
||||
_publish_local_config_update("node_thread_counts")
|
||||
_publish_local_config_update("runtime_settings")
|
||||
_publish_local_config_update("sensitive_words")
|
||||
|
||||
_LAST_RUNTIME_CONFIG_HASH = bundle_hash
|
||||
_log(
|
||||
"runtime config applied: "
|
||||
f"thread_count={thread_count} "
|
||||
f"node_override={node_thread_counts.get(NODE_CODE)} "
|
||||
f"sensitive_words={int(sensitive_words.get('total', 0) or 0)}"
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def _hostname() -> str:
|
||||
generic_values = {"localhost", "localhost.localdomain", "ip6-localhost", "localhost6"}
|
||||
candidates: list[str] = []
|
||||
try:
|
||||
candidates.append(socket.gethostname())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
candidates.append(socket.getfqdn())
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
candidates.append(os.uname().nodename)
|
||||
except Exception:
|
||||
pass
|
||||
candidates.append(str(os.getenv("HOSTNAME", "")).strip())
|
||||
|
||||
for value in candidates:
|
||||
normalized = str(value or "").strip()
|
||||
if not normalized:
|
||||
continue
|
||||
if normalized.lower() in generic_values:
|
||||
continue
|
||||
return normalized
|
||||
return NODE_CODE or ""
|
||||
|
||||
|
||||
def _control_plane_host_port() -> tuple[str, int]:
|
||||
parsed = urlparse(CONTROL_PLANE_BASE_URL if "://" in CONTROL_PLANE_BASE_URL else f"http://{CONTROL_PLANE_BASE_URL}")
|
||||
host = str(parsed.hostname or "").strip()
|
||||
if not host:
|
||||
return "", 0
|
||||
port = int(parsed.port or (443 if parsed.scheme == "https" else 80))
|
||||
return host, port
|
||||
|
||||
|
||||
def _first_non_loopback_ip(values: list[str]) -> str:
|
||||
for value in values:
|
||||
normalized = str(value or "").strip()
|
||||
if not normalized or normalized.startswith("127.") or normalized == "::1":
|
||||
continue
|
||||
return normalized
|
||||
return ""
|
||||
|
||||
|
||||
def _ip() -> str:
|
||||
explicit_ip = str(os.getenv("OPS_AGENT_IP", "")).strip()
|
||||
if explicit_ip:
|
||||
return explicit_ip
|
||||
|
||||
host, port = _control_plane_host_port()
|
||||
if host and port:
|
||||
for family in (socket.AF_INET, socket.AF_INET6):
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(family, socket.SOCK_DGRAM)
|
||||
sock.connect((host, port))
|
||||
local_ip = str(sock.getsockname()[0] or "").strip()
|
||||
if local_ip and not local_ip.startswith("127.") and local_ip != "::1":
|
||||
return local_ip
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
if sock is not None:
|
||||
try:
|
||||
sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
hostname = _hostname()
|
||||
if hostname:
|
||||
try:
|
||||
addrinfo = socket.getaddrinfo(hostname, None)
|
||||
resolved_ips = [str(item[4][0] or "").strip() for item in addrinfo if item and len(item) >= 5]
|
||||
best_ip = _first_non_loopback_ip(resolved_ips)
|
||||
if best_ip:
|
||||
return best_ip
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
return socket.gethostbyname(socket.gethostname())
|
||||
fallback_ip = str(socket.gethostbyname(socket.gethostname()) or "").strip()
|
||||
if fallback_ip and not fallback_ip.startswith("127."):
|
||||
return fallback_ip
|
||||
except Exception:
|
||||
return ""
|
||||
pass
|
||||
return ""
|
||||
|
||||
|
||||
def _detect_runtime_snapshot() -> dict:
|
||||
worker_runtime = {}
|
||||
try:
|
||||
from app.services.worker_control_service import detect_worker_runtime
|
||||
|
||||
worker_runtime = detect_worker_runtime() or {}
|
||||
except Exception as exc:
|
||||
worker_runtime = {
|
||||
"running": False,
|
||||
"process_count": 0,
|
||||
"latest_start_time": "",
|
||||
"message": "",
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
try:
|
||||
from app.services.detect_service import get_detect_status
|
||||
|
||||
detect_status = get_detect_status() or {}
|
||||
runtime_state = detect_status.get("runtime_state") if isinstance(detect_status.get("runtime_state"), dict) else {}
|
||||
active_job = detect_status.get("active_job") if isinstance(detect_status.get("active_job"), dict) else {}
|
||||
|
||||
active_threads = max(0, int(detect_status.get("active_thread_count", 0) or 0))
|
||||
max_threads = max(0, int(detect_status.get("max_thread_count", 0) or 0))
|
||||
current_load = max(
|
||||
active_threads,
|
||||
int(active_job.get("items_running", 0) or 0),
|
||||
)
|
||||
phase_label = str(detect_status.get("phase_label") or "").strip()
|
||||
phase_detail = str(
|
||||
detect_status.get("phase_detail")
|
||||
or detect_status.get("recent_event")
|
||||
or detect_status.get("worker_runtime_message")
|
||||
or ""
|
||||
).strip()
|
||||
|
||||
inferred_worker_online = bool(
|
||||
worker_runtime.get("running", False)
|
||||
or detect_status.get("worker_online", False)
|
||||
or runtime_state.get("service_running", False)
|
||||
or current_load > 0
|
||||
or active_threads > 0
|
||||
)
|
||||
return {
|
||||
"worker_online": inferred_worker_online,
|
||||
"service_running": bool(
|
||||
runtime_state.get("service_running", False)
|
||||
or worker_runtime.get("running", False)
|
||||
or inferred_worker_online
|
||||
),
|
||||
"detecting": bool(
|
||||
detect_status.get("detecting", False)
|
||||
or runtime_state.get("detecting", False)
|
||||
or current_load > 0
|
||||
or active_threads > 0
|
||||
),
|
||||
"active_threads": active_threads,
|
||||
"max_threads": max_threads,
|
||||
"current_load": current_load,
|
||||
"phase_label": phase_label,
|
||||
"phase_detail": phase_detail,
|
||||
"recent_warning": str(detect_status.get("recent_warning") or "").strip(),
|
||||
"updated_at": str(runtime_state.get("updated_at") or "").strip(),
|
||||
"detect_participating": bool(
|
||||
detect_status.get("detect_participating", False)
|
||||
or current_load > 0
|
||||
or active_threads > 0
|
||||
),
|
||||
}
|
||||
except Exception as exc:
|
||||
worker_online = bool(worker_runtime.get("running", False))
|
||||
worker_message = str(worker_runtime.get("message") or "").strip()
|
||||
return {
|
||||
"worker_online": worker_online,
|
||||
"service_running": worker_online,
|
||||
"detecting": False,
|
||||
"active_threads": 0,
|
||||
"max_threads": 0,
|
||||
"current_load": 0,
|
||||
"phase_label": "",
|
||||
"phase_detail": worker_message,
|
||||
"recent_warning": "",
|
||||
"updated_at": str(worker_runtime.get("latest_start_time") or "").strip(),
|
||||
"detect_participating": False,
|
||||
"error": str(exc),
|
||||
}
|
||||
|
||||
|
||||
def _base_payload() -> dict:
|
||||
@@ -651,12 +958,37 @@ def _base_payload() -> dict:
|
||||
"node_agent": NODE_AGENT_SERVICE_NAME,
|
||||
},
|
||||
"delivery_queue": _delivery_queue_snapshot(),
|
||||
"detect_runtime": _detect_runtime_snapshot(),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _run(command: list[str], timeout: int = 60) -> tuple[int, str, str]:
|
||||
completed = subprocess.run(command, capture_output=True, text=True, timeout=timeout)
|
||||
normalized_command = [str(part or "").strip() for part in command]
|
||||
combined_output = f"{completed.stdout or ''}\n{completed.stderr or ''}".lower()
|
||||
needs_sudo_retry = (
|
||||
normalized_command
|
||||
and normalized_command[0] == "systemctl"
|
||||
and completed.returncode != 0
|
||||
and "sudo" not in normalized_command
|
||||
and any(
|
||||
marker in combined_output
|
||||
for marker in (
|
||||
"interactive authentication required",
|
||||
"authentication is required",
|
||||
"authorization not available",
|
||||
"polkit",
|
||||
)
|
||||
)
|
||||
)
|
||||
if needs_sudo_retry:
|
||||
completed = subprocess.run(
|
||||
["sudo", "-n", *normalized_command],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
)
|
||||
return completed.returncode, completed.stdout.strip(), completed.stderr.strip()
|
||||
|
||||
|
||||
@@ -727,18 +1059,22 @@ def _execute_action(
|
||||
|
||||
|
||||
def _register() -> None:
|
||||
response = _post("/api/v1/ops/agent/register", _base_payload())
|
||||
response = _post("/api/v1/ops/agent/register", _base_payload(), timeout=AGENT_REGISTER_TIMEOUT_SECONDS)
|
||||
_ensure_ok_response(response, "agent register failed")
|
||||
_log(f"registered: {response.get('message')}")
|
||||
|
||||
|
||||
def _heartbeat() -> None:
|
||||
response = _post("/api/v1/ops/agent/heartbeat", _base_payload())
|
||||
response = _post("/api/v1/ops/agent/heartbeat", _base_payload(), timeout=AGENT_HEARTBEAT_TIMEOUT_SECONDS)
|
||||
_ensure_ok_response(response, "agent heartbeat failed")
|
||||
|
||||
|
||||
def _pull_jobs() -> list[dict]:
|
||||
response = _post(f"/api/v1/ops/agent/pull?limit=1", {"node_code": NODE_CODE})
|
||||
response = _post(
|
||||
f"/api/v1/ops/agent/pull?limit=1",
|
||||
{"node_code": NODE_CODE},
|
||||
timeout=AGENT_PULL_TIMEOUT_SECONDS,
|
||||
)
|
||||
_ensure_ok_response(response, "agent pull failed")
|
||||
data = response.get("data") or {}
|
||||
return list(data.get("jobs") or [])
|
||||
@@ -847,7 +1183,7 @@ def _job_complete(
|
||||
path=f"/api/v1/ops/agent/jobs/{job_id}/complete",
|
||||
payload=payload,
|
||||
request_id=request_id,
|
||||
timeout=30,
|
||||
timeout=AGENT_JOB_COMPLETE_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -883,7 +1219,7 @@ def _job_event(
|
||||
path=f"/api/v1/ops/agent/jobs/{job_id}/events",
|
||||
payload=delivery_payload,
|
||||
request_id=request_id,
|
||||
timeout=15,
|
||||
timeout=AGENT_JOB_EVENT_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -931,7 +1267,22 @@ def _process_job(job: dict) -> None:
|
||||
"start_delivery_error": start_delivery_error,
|
||||
},
|
||||
)
|
||||
ok, message, result = _execute_action(action, payload, job_id=job_id, job_context=normalized_job)
|
||||
try:
|
||||
ok, message, result = _execute_action(action, payload, job_id=job_id, job_context=normalized_job)
|
||||
except Exception as exc:
|
||||
ok = False
|
||||
message = f"executor exception: {exc}"
|
||||
result = {
|
||||
"stdout": "",
|
||||
"stderr": traceback.format_exc(),
|
||||
"summary_text": message,
|
||||
"exception_type": exc.__class__.__name__,
|
||||
}
|
||||
_log(
|
||||
"job execute exception: "
|
||||
f"id={job_id} code={normalized_job.get('job_code') or '-'} "
|
||||
f"action={action} error={exc}"
|
||||
)
|
||||
stdout = str(result.get("stdout") or "")
|
||||
stderr = str(result.get("stderr") or "")
|
||||
duration_ms = max(0, int((time.monotonic() - started_at) * 1000))
|
||||
@@ -956,6 +1307,57 @@ def _process_job(job: dict) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _process_job_with_guard(job: dict) -> None:
|
||||
normalized_job = _normalize_agent_job(job)
|
||||
job_id = int(normalized_job.get("job_id") or 0)
|
||||
action = str(normalized_job.get("action") or "").strip()
|
||||
started_at = time.monotonic()
|
||||
try:
|
||||
_process_job(normalized_job)
|
||||
except Exception as exc:
|
||||
duration_ms = max(0, int((time.monotonic() - started_at) * 1000))
|
||||
traceback_text = traceback.format_exc()
|
||||
summary_text = f"node-agent fatal exception: {exc}"
|
||||
_log(
|
||||
"job fatal exception: "
|
||||
f"id={job_id} code={normalized_job.get('job_code') or '-'} "
|
||||
f"action={action or '-'} error={exc}"
|
||||
)
|
||||
if job_id <= 0:
|
||||
raise
|
||||
try:
|
||||
delivery = _job_complete(
|
||||
job_id,
|
||||
status="failed",
|
||||
stdout="",
|
||||
stderr=traceback_text,
|
||||
result={
|
||||
"stdout": "",
|
||||
"stderr": traceback_text,
|
||||
"summary_text": summary_text,
|
||||
"exception_type": exc.__class__.__name__,
|
||||
"fatal_loop_exception": True,
|
||||
},
|
||||
error_message=summary_text,
|
||||
duration_ms=duration_ms,
|
||||
summary_text=summary_text,
|
||||
focus_ref=dict(normalized_job.get("focus_ref") or {}),
|
||||
step_ref=dict(normalized_job.get("step_ref") or {}),
|
||||
release_context=dict(normalized_job.get("release_context") or {}),
|
||||
)
|
||||
_log(
|
||||
"job fatal exception completion: "
|
||||
f"id={job_id} code={normalized_job.get('job_code') or '-'} "
|
||||
f"action={action or '-'} delivery={delivery.get('state')}"
|
||||
)
|
||||
except Exception as completion_exc:
|
||||
_log(
|
||||
"job fatal exception completion failed: "
|
||||
f"id={job_id} code={normalized_job.get('job_code') or '-'} "
|
||||
f"action={action or '-'} error={completion_exc}"
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not NODE_CODE:
|
||||
raise RuntimeError("NODE_CODE 未配置")
|
||||
@@ -963,6 +1365,7 @@ def main() -> None:
|
||||
_ensure_queue_dirs()
|
||||
_register()
|
||||
last_heartbeat_at = 0.0
|
||||
last_runtime_config_sync_at = 0.0
|
||||
|
||||
while True:
|
||||
now = time.time()
|
||||
@@ -970,13 +1373,17 @@ def main() -> None:
|
||||
delivery_summary = _flush_delivery_queue(limit=AGENT_QUEUE_FLUSH_LIMIT)
|
||||
if delivery_summary["delivered"] or delivery_summary["dead_letter"]:
|
||||
_log(f"delivery queue flush: {delivery_summary}")
|
||||
if now - last_runtime_config_sync_at >= AGENT_RUNTIME_CONFIG_SYNC_INTERVAL_SECONDS:
|
||||
bundle = _pull_runtime_config()
|
||||
_apply_runtime_config(bundle)
|
||||
last_runtime_config_sync_at = now
|
||||
if now - last_heartbeat_at >= 15:
|
||||
_heartbeat()
|
||||
last_heartbeat_at = now
|
||||
jobs = _pull_jobs()
|
||||
if jobs:
|
||||
for job in jobs:
|
||||
_process_job(job)
|
||||
_process_job_with_guard(job)
|
||||
else:
|
||||
time.sleep(AGENT_POLL_INTERVAL_SECONDS)
|
||||
except urllib.error.HTTPError as exc:
|
||||
|
||||
Reference in New Issue
Block a user